claude-coder 1.9.0 → 1.9.2
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 +214 -214
- package/bin/cli.js +155 -155
- package/package.json +55 -55
- package/recipes/_shared/roles/developer.md +11 -11
- package/recipes/_shared/roles/product.md +12 -12
- package/recipes/_shared/roles/tester.md +12 -12
- package/recipes/_shared/test/report-format.md +86 -86
- package/recipes/backend/base.md +27 -27
- package/recipes/backend/components/auth.md +18 -18
- package/recipes/backend/components/crud-api.md +18 -18
- package/recipes/backend/components/file-service.md +15 -15
- package/recipes/backend/manifest.json +20 -20
- package/recipes/backend/test/api-test.md +25 -25
- package/recipes/console/base.md +37 -37
- package/recipes/console/components/modal-form.md +20 -20
- package/recipes/console/components/pagination.md +17 -17
- package/recipes/console/components/search.md +17 -17
- package/recipes/console/components/table-list.md +18 -18
- package/recipes/console/components/tabs.md +14 -14
- package/recipes/console/components/tree.md +15 -15
- package/recipes/console/components/upload.md +15 -15
- package/recipes/console/manifest.json +24 -24
- package/recipes/console/test/crud-e2e.md +47 -47
- package/recipes/h5/base.md +26 -26
- package/recipes/h5/components/animation.md +11 -11
- package/recipes/h5/components/countdown.md +11 -11
- package/recipes/h5/components/share.md +11 -11
- package/recipes/h5/components/swiper.md +11 -11
- package/recipes/h5/manifest.json +21 -21
- package/recipes/h5/test/h5-e2e.md +20 -20
- package/src/commands/auth.js +420 -362
- package/src/commands/setup-modules/helpers.js +100 -100
- package/src/commands/setup-modules/index.js +25 -25
- package/src/commands/setup-modules/mcp.js +115 -115
- package/src/commands/setup-modules/provider.js +260 -260
- package/src/commands/setup-modules/safety.js +47 -47
- package/src/commands/setup-modules/simplify.js +52 -52
- package/src/commands/setup.js +172 -172
- package/src/common/assets.js +245 -245
- package/src/common/config.js +125 -125
- package/src/common/constants.js +55 -55
- package/src/common/indicator.js +260 -260
- package/src/common/interaction.js +170 -170
- package/src/common/logging.js +77 -77
- package/src/common/sdk.js +50 -50
- package/src/common/tasks.js +88 -88
- package/src/common/utils.js +213 -213
- package/src/core/coding.js +33 -33
- package/src/core/go.js +264 -264
- package/src/core/hooks.js +500 -500
- package/src/core/init.js +166 -165
- package/src/core/plan.js +188 -187
- package/src/core/prompts.js +247 -247
- package/src/core/repair.js +36 -36
- package/src/core/runner.js +471 -458
- package/src/core/scan.js +93 -93
- package/src/core/session.js +280 -271
- package/src/core/simplify.js +74 -74
- package/src/core/state.js +105 -105
- package/src/index.js +76 -76
- package/templates/bash-process.md +12 -12
- package/templates/codingSystem.md +65 -65
- package/templates/codingUser.md +17 -17
- package/templates/coreProtocol.md +29 -29
- package/templates/goSystem.md +130 -130
- package/templates/guidance.json +72 -72
- package/templates/planSystem.md +78 -78
- package/templates/planUser.md +8 -8
- package/templates/requirements.example.md +57 -57
- package/templates/scanSystem.md +120 -120
- package/templates/scanUser.md +10 -10
- package/templates/test_rule.md +194 -194
- package/templates/web-testing.md +17 -17
- package/types/index.d.ts +217 -217
package/src/common/indicator.js
CHANGED
|
@@ -1,260 +1,260 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { COLOR } = require('./config');
|
|
4
|
-
const { localTimestamp, truncateMiddle } = require('./utils');
|
|
5
|
-
|
|
6
|
-
const SPINNERS = ['⠋', '⠙', '⠸', '⠴', '⠦', '⠇'];
|
|
7
|
-
|
|
8
|
-
function termCols() {
|
|
9
|
-
return process.stderr.columns
|
|
10
|
-
|| process.stdout.columns
|
|
11
|
-
|| parseInt(process.env.COLUMNS, 10)
|
|
12
|
-
|| 70;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
class Indicator {
|
|
16
|
-
constructor() {
|
|
17
|
-
this.phase = 'thinking';
|
|
18
|
-
this.spinnerIndex = 0;
|
|
19
|
-
this.timer = null;
|
|
20
|
-
this.lastActivityTime = Date.now();
|
|
21
|
-
this.sessionNum = 0;
|
|
22
|
-
this.startTime = Date.now();
|
|
23
|
-
this.stallTimeoutMin = 30;
|
|
24
|
-
this.toolRunning = false;
|
|
25
|
-
this.toolStartTime = 0;
|
|
26
|
-
this._paused = false;
|
|
27
|
-
this.projectRoot = '';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
start(sessionNum, stallTimeoutMin, projectRoot) {
|
|
31
|
-
this.sessionNum = sessionNum;
|
|
32
|
-
this.startTime = Date.now();
|
|
33
|
-
this.lastActivityTime = Date.now();
|
|
34
|
-
if (stallTimeoutMin > 0) this.stallTimeoutMin = stallTimeoutMin;
|
|
35
|
-
if (projectRoot) this.projectRoot = projectRoot;
|
|
36
|
-
this.timer = setInterval(() => this._render(), 1000);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
stop() {
|
|
40
|
-
if (this.timer) { clearInterval(this.timer); this.timer = null; }
|
|
41
|
-
process.stderr.write('\r\x1b[K');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
updatePhase(phase) { this.phase = phase; }
|
|
45
|
-
updateActivity() { this.lastActivityTime = Date.now(); }
|
|
46
|
-
|
|
47
|
-
startTool() {
|
|
48
|
-
this.toolRunning = true;
|
|
49
|
-
this.toolStartTime = Date.now();
|
|
50
|
-
this.lastActivityTime = Date.now();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
endTool() {
|
|
54
|
-
if (!this.toolRunning) return;
|
|
55
|
-
this.toolRunning = false;
|
|
56
|
-
this.lastActivityTime = Date.now();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
pauseRendering() { this._paused = true; }
|
|
60
|
-
resumeRendering() { this._paused = false; }
|
|
61
|
-
|
|
62
|
-
_render() {
|
|
63
|
-
if (this._paused) return;
|
|
64
|
-
this.spinnerIndex++;
|
|
65
|
-
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
|
66
|
-
const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
|
67
|
-
const ss = String(elapsed % 60).padStart(2, '0');
|
|
68
|
-
const spinner = SPINNERS[this.spinnerIndex % SPINNERS.length];
|
|
69
|
-
const phaseLabel = this.phase === 'thinking'
|
|
70
|
-
? `${COLOR.yellow}思考中${COLOR.reset}`
|
|
71
|
-
: `${COLOR.green}编码中${COLOR.reset}`;
|
|
72
|
-
|
|
73
|
-
const idleMs = Date.now() - this.lastActivityTime;
|
|
74
|
-
const idleMin = Math.floor(idleMs / 60000);
|
|
75
|
-
|
|
76
|
-
let line = `${spinner} S${this.sessionNum} ${mm}:${ss} ${phaseLabel}`;
|
|
77
|
-
if (idleMin >= 2) {
|
|
78
|
-
if (this.toolRunning) {
|
|
79
|
-
const sec = Math.floor((Date.now() - this.toolStartTime) / 1000);
|
|
80
|
-
line += ` ${COLOR.yellow}工具执行中 ${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, '0')}${COLOR.reset}`;
|
|
81
|
-
} else {
|
|
82
|
-
line += ` ${COLOR.red}${idleMin}分无响应${COLOR.reset}`;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
process.stderr.write(`\r\x1b[K${line}`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ─── Path helpers ────────────────────────────────────────
|
|
90
|
-
|
|
91
|
-
function normalizePath(raw, projectRoot) {
|
|
92
|
-
if (!raw) return '';
|
|
93
|
-
if (projectRoot && raw.startsWith(projectRoot)) {
|
|
94
|
-
const rel = raw.slice(projectRoot.length);
|
|
95
|
-
return rel
|
|
96
|
-
}
|
|
97
|
-
const home = process.env.HOME || '';
|
|
98
|
-
if (home && raw.startsWith(home)) return '~' + raw.slice(home.length);
|
|
99
|
-
const parts = raw.split(
|
|
100
|
-
return parts.length > 3 ? '.../' + parts.slice(-3).join('/') : raw;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function stripAbsolutePaths(str, projectRoot) {
|
|
104
|
-
let result = str;
|
|
105
|
-
if (projectRoot) {
|
|
106
|
-
result = result.replace(new RegExp(projectRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '
|
|
107
|
-
}
|
|
108
|
-
const home = process.env.HOME || '';
|
|
109
|
-
if (home) {
|
|
110
|
-
result = result.replace(new RegExp(home.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '
|
|
111
|
-
}
|
|
112
|
-
return result;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function extractTarget(input, projectRoot) {
|
|
116
|
-
if (!input || typeof input !== 'object') return '';
|
|
117
|
-
const filePath = input.file_path || input.path || '';
|
|
118
|
-
if (filePath) return normalizePath(filePath, projectRoot);
|
|
119
|
-
const cmd = input.command || '';
|
|
120
|
-
if (cmd) return stripAbsolutePaths(extractBashCore(cmd), projectRoot);
|
|
121
|
-
const pattern = input.pattern || '';
|
|
122
|
-
if (pattern) return `pattern: ${pattern}`;
|
|
123
|
-
return '';
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ─── Bash helpers ────────────────────────────────────────
|
|
127
|
-
|
|
128
|
-
function extractBashLabel(cmd) {
|
|
129
|
-
if (cmd.includes('git ')) return 'Git';
|
|
130
|
-
if (/\b(npm|pnpm|yarn|pip)\b/.test(cmd)) return cmd.match(/\b(npm|pnpm|yarn|pip)\b/)[0];
|
|
131
|
-
if (/\b(sleep|Start-Sleep|timeout\s+\/t)\b/i.test(cmd)) return '等待';
|
|
132
|
-
if (cmd.includes('curl')) return '网络';
|
|
133
|
-
if (/\b(pytest|jest|test)\b/.test(cmd)) return '测试';
|
|
134
|
-
if (/\b(python|node)\s/.test(cmd)) return '执行';
|
|
135
|
-
return '执行';
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function extractCurlUrl(cmd) {
|
|
139
|
-
const m = cmd.match(/https?:\/\/\S+/);
|
|
140
|
-
return m ? m[0].replace(/['";)}\]>]+$/, '') : null;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function extractBashCore(cmd) {
|
|
144
|
-
let clean = cmd.replace(/^(?:(?:cd|source|export)\s+\S+\s*&&\s*)+/g, '').trim();
|
|
145
|
-
clean = clean.replace(/"[^"]*"/g, m => m.replace(/[;|&]/g, '\x00'));
|
|
146
|
-
clean = clean.replace(/'[^']*'/g, m => m.replace(/[;|&]/g, '\x00'));
|
|
147
|
-
clean = clean.split(/\s*(?:\|\|?|;|&&|2>&1|2>\/dev\/null|>\s*\/dev\/null)\s*/)[0];
|
|
148
|
-
clean = clean.replace(/\x00/g, ';').trim();
|
|
149
|
-
clean = clean.replace(/\s*<<\s*['"]?\w+['"]?\s*$/, '');
|
|
150
|
-
return clean;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ─── inferPhaseStep: 输出永久工具行 ─────────────────────
|
|
154
|
-
|
|
155
|
-
function formatElapsed(indicator) {
|
|
156
|
-
const elapsed = Math.floor((Date.now() - indicator.startTime) / 1000);
|
|
157
|
-
const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
|
158
|
-
const ss = String(elapsed % 60).padStart(2, '0');
|
|
159
|
-
return `${mm}:${ss}`;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const CODING_TOOLS = /^(write|edit|multiedit|str_replace_editor|strreplace)$/;
|
|
163
|
-
const READ_TOOLS = /^(read|glob|grep|ls)$/;
|
|
164
|
-
|
|
165
|
-
function inferPhaseStep(indicator, toolName, toolInput) {
|
|
166
|
-
const name = (toolName || '').toLowerCase();
|
|
167
|
-
const displayName = toolName || name;
|
|
168
|
-
const pr = indicator.projectRoot || '';
|
|
169
|
-
const cols = termCols();
|
|
170
|
-
|
|
171
|
-
indicator.startTool();
|
|
172
|
-
|
|
173
|
-
let step, target;
|
|
174
|
-
|
|
175
|
-
if (CODING_TOOLS.test(name)) {
|
|
176
|
-
indicator.updatePhase('coding');
|
|
177
|
-
step = displayName;
|
|
178
|
-
target = normalizePath(
|
|
179
|
-
(typeof toolInput === 'object' ? (toolInput.file_path || toolInput.path || '') : ''), pr
|
|
180
|
-
);
|
|
181
|
-
} else if (name === 'bash' || name === 'shell') {
|
|
182
|
-
const cmd = typeof toolInput === 'object' ? (toolInput.command || '') : String(toolInput || '');
|
|
183
|
-
const label = extractBashLabel(cmd);
|
|
184
|
-
step = displayName;
|
|
185
|
-
const url = (label === '网络') ? extractCurlUrl(cmd) : null;
|
|
186
|
-
target = url || stripAbsolutePaths(extractBashCore(cmd), pr);
|
|
187
|
-
if (['测试', '执行'].includes(label)) indicator.updatePhase('coding');
|
|
188
|
-
} else if (READ_TOOLS.test(name)) {
|
|
189
|
-
indicator.updatePhase('thinking');
|
|
190
|
-
step = displayName;
|
|
191
|
-
target = extractTarget(toolInput, pr);
|
|
192
|
-
} else if (name === 'task') {
|
|
193
|
-
indicator.updatePhase('thinking');
|
|
194
|
-
step = displayName;
|
|
195
|
-
target = '';
|
|
196
|
-
} else if (name === 'websearch' || name === 'webfetch') {
|
|
197
|
-
indicator.updatePhase('thinking');
|
|
198
|
-
step = displayName;
|
|
199
|
-
target = '';
|
|
200
|
-
} else if (name.startsWith('mcp__')) {
|
|
201
|
-
indicator.updatePhase('coding');
|
|
202
|
-
step = name.split('__').pop() || displayName;
|
|
203
|
-
target = typeof toolInput === 'object'
|
|
204
|
-
? String(toolInput.url || toolInput.text || toolInput.element || '').slice(0, 60)
|
|
205
|
-
: '';
|
|
206
|
-
} else {
|
|
207
|
-
step = displayName;
|
|
208
|
-
target = '';
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const time = localTimestamp();
|
|
212
|
-
const el = formatElapsed(indicator);
|
|
213
|
-
let line = ` ${COLOR.dim}${time}${COLOR.reset} ${COLOR.dim}${el}${COLOR.reset} ${step}`;
|
|
214
|
-
if (target) {
|
|
215
|
-
const maxTarget = Math.max(10, cols - displayWidth(stripAnsi(line)) - 3);
|
|
216
|
-
line += ` ${truncateMiddle(target, maxTarget)}`;
|
|
217
|
-
}
|
|
218
|
-
process.stderr.write(`\r\x1b[K${clampLine(line, cols)}\n`);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ─── Terminal width helpers ──────────────────────────────
|
|
222
|
-
|
|
223
|
-
function stripAnsi(str) {
|
|
224
|
-
return str.replace(/\x1b\[[^m]*m/g, '');
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function isWideChar(cp) {
|
|
228
|
-
return (cp >= 0x4E00 && cp <= 0x9FFF)
|
|
229
|
-
|| (cp >= 0x3400 && cp <= 0x4DBF)
|
|
230
|
-
|| (cp >= 0x3000 && cp <= 0x30FF)
|
|
231
|
-
|| (cp >= 0xF900 && cp <= 0xFAFF)
|
|
232
|
-
|| (cp >= 0xFF01 && cp <= 0xFF60)
|
|
233
|
-
|| (cp >= 0xFFE0 && cp <= 0xFFE6)
|
|
234
|
-
|| (cp >= 0xAC00 && cp <= 0xD7AF)
|
|
235
|
-
|| (cp >= 0x20000 && cp <= 0x2FA1F);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function displayWidth(str) {
|
|
239
|
-
let w = 0;
|
|
240
|
-
for (const ch of str) {
|
|
241
|
-
w += isWideChar(ch.codePointAt(0)) ? 2 : 1;
|
|
242
|
-
}
|
|
243
|
-
return w;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function clampLine(line, cols) {
|
|
247
|
-
const max = cols - 1;
|
|
248
|
-
if (displayWidth(stripAnsi(line)) <= max) return line;
|
|
249
|
-
let w = 0, cut = 0, esc = false;
|
|
250
|
-
for (let i = 0; i < line.length; i++) {
|
|
251
|
-
if (line[i] === '\x1b') esc = true;
|
|
252
|
-
if (esc) { if (line[i] === 'm') esc = false; continue; }
|
|
253
|
-
const cw = isWideChar(line.codePointAt(i)) ? 2 : 1;
|
|
254
|
-
if (w + cw >= max) { cut = i; break; }
|
|
255
|
-
w += cw;
|
|
256
|
-
}
|
|
257
|
-
return line.slice(0, cut) + '…' + COLOR.reset;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
module.exports = { Indicator, inferPhaseStep };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { COLOR } = require('./config');
|
|
4
|
+
const { localTimestamp, truncateMiddle } = require('./utils');
|
|
5
|
+
|
|
6
|
+
const SPINNERS = ['⠋', '⠙', '⠸', '⠴', '⠦', '⠇'];
|
|
7
|
+
|
|
8
|
+
function termCols() {
|
|
9
|
+
return process.stderr.columns
|
|
10
|
+
|| process.stdout.columns
|
|
11
|
+
|| parseInt(process.env.COLUMNS, 10)
|
|
12
|
+
|| 70;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class Indicator {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.phase = 'thinking';
|
|
18
|
+
this.spinnerIndex = 0;
|
|
19
|
+
this.timer = null;
|
|
20
|
+
this.lastActivityTime = Date.now();
|
|
21
|
+
this.sessionNum = 0;
|
|
22
|
+
this.startTime = Date.now();
|
|
23
|
+
this.stallTimeoutMin = 30;
|
|
24
|
+
this.toolRunning = false;
|
|
25
|
+
this.toolStartTime = 0;
|
|
26
|
+
this._paused = false;
|
|
27
|
+
this.projectRoot = '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
start(sessionNum, stallTimeoutMin, projectRoot) {
|
|
31
|
+
this.sessionNum = sessionNum;
|
|
32
|
+
this.startTime = Date.now();
|
|
33
|
+
this.lastActivityTime = Date.now();
|
|
34
|
+
if (stallTimeoutMin > 0) this.stallTimeoutMin = stallTimeoutMin;
|
|
35
|
+
if (projectRoot) this.projectRoot = projectRoot;
|
|
36
|
+
this.timer = setInterval(() => this._render(), 1000);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
stop() {
|
|
40
|
+
if (this.timer) { clearInterval(this.timer); this.timer = null; }
|
|
41
|
+
process.stderr.write('\r\x1b[K');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
updatePhase(phase) { this.phase = phase; }
|
|
45
|
+
updateActivity() { this.lastActivityTime = Date.now(); }
|
|
46
|
+
|
|
47
|
+
startTool() {
|
|
48
|
+
this.toolRunning = true;
|
|
49
|
+
this.toolStartTime = Date.now();
|
|
50
|
+
this.lastActivityTime = Date.now();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
endTool() {
|
|
54
|
+
if (!this.toolRunning) return;
|
|
55
|
+
this.toolRunning = false;
|
|
56
|
+
this.lastActivityTime = Date.now();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
pauseRendering() { this._paused = true; }
|
|
60
|
+
resumeRendering() { this._paused = false; }
|
|
61
|
+
|
|
62
|
+
_render() {
|
|
63
|
+
if (this._paused) return;
|
|
64
|
+
this.spinnerIndex++;
|
|
65
|
+
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
|
66
|
+
const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
|
67
|
+
const ss = String(elapsed % 60).padStart(2, '0');
|
|
68
|
+
const spinner = SPINNERS[this.spinnerIndex % SPINNERS.length];
|
|
69
|
+
const phaseLabel = this.phase === 'thinking'
|
|
70
|
+
? `${COLOR.yellow}思考中${COLOR.reset}`
|
|
71
|
+
: `${COLOR.green}编码中${COLOR.reset}`;
|
|
72
|
+
|
|
73
|
+
const idleMs = Date.now() - this.lastActivityTime;
|
|
74
|
+
const idleMin = Math.floor(idleMs / 60000);
|
|
75
|
+
|
|
76
|
+
let line = `${spinner} S${this.sessionNum} ${mm}:${ss} ${phaseLabel}`;
|
|
77
|
+
if (idleMin >= 2) {
|
|
78
|
+
if (this.toolRunning) {
|
|
79
|
+
const sec = Math.floor((Date.now() - this.toolStartTime) / 1000);
|
|
80
|
+
line += ` ${COLOR.yellow}工具执行中 ${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, '0')}${COLOR.reset}`;
|
|
81
|
+
} else {
|
|
82
|
+
line += ` ${COLOR.red}${idleMin}分无响应${COLOR.reset}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
process.stderr.write(`\r\x1b[K${line}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Path helpers ────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function normalizePath(raw, projectRoot) {
|
|
92
|
+
if (!raw) return '';
|
|
93
|
+
if (projectRoot && raw.startsWith(projectRoot)) {
|
|
94
|
+
const rel = raw.slice(projectRoot.length);
|
|
95
|
+
return (rel[0] === '/' || rel[0] === '\\') ? rel.slice(1) : rel;
|
|
96
|
+
}
|
|
97
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
98
|
+
if (home && raw.startsWith(home)) return '~' + raw.slice(home.length);
|
|
99
|
+
const parts = raw.split(/[/\\]/).filter(Boolean);
|
|
100
|
+
return parts.length > 3 ? '.../' + parts.slice(-3).join('/') : raw;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function stripAbsolutePaths(str, projectRoot) {
|
|
104
|
+
let result = str;
|
|
105
|
+
if (projectRoot) {
|
|
106
|
+
result = result.replace(new RegExp(projectRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[/\\\\]?', 'g'), './');
|
|
107
|
+
}
|
|
108
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
109
|
+
if (home) {
|
|
110
|
+
result = result.replace(new RegExp(home.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[/\\\\]?', 'g'), '~/');
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function extractTarget(input, projectRoot) {
|
|
116
|
+
if (!input || typeof input !== 'object') return '';
|
|
117
|
+
const filePath = input.file_path || input.path || '';
|
|
118
|
+
if (filePath) return normalizePath(filePath, projectRoot);
|
|
119
|
+
const cmd = input.command || '';
|
|
120
|
+
if (cmd) return stripAbsolutePaths(extractBashCore(cmd), projectRoot);
|
|
121
|
+
const pattern = input.pattern || '';
|
|
122
|
+
if (pattern) return `pattern: ${pattern}`;
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Bash helpers ────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
function extractBashLabel(cmd) {
|
|
129
|
+
if (cmd.includes('git ')) return 'Git';
|
|
130
|
+
if (/\b(npm|pnpm|yarn|pip)\b/.test(cmd)) return cmd.match(/\b(npm|pnpm|yarn|pip)\b/)[0];
|
|
131
|
+
if (/\b(sleep|Start-Sleep|timeout\s+\/t)\b/i.test(cmd)) return '等待';
|
|
132
|
+
if (cmd.includes('curl')) return '网络';
|
|
133
|
+
if (/\b(pytest|jest|test)\b/.test(cmd)) return '测试';
|
|
134
|
+
if (/\b(python|node)\s/.test(cmd)) return '执行';
|
|
135
|
+
return '执行';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extractCurlUrl(cmd) {
|
|
139
|
+
const m = cmd.match(/https?:\/\/\S+/);
|
|
140
|
+
return m ? m[0].replace(/['";)}\]>]+$/, '') : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function extractBashCore(cmd) {
|
|
144
|
+
let clean = cmd.replace(/^(?:(?:cd|source|export)\s+\S+\s*&&\s*)+/g, '').trim();
|
|
145
|
+
clean = clean.replace(/"[^"]*"/g, m => m.replace(/[;|&]/g, '\x00'));
|
|
146
|
+
clean = clean.replace(/'[^']*'/g, m => m.replace(/[;|&]/g, '\x00'));
|
|
147
|
+
clean = clean.split(/\s*(?:\|\|?|;|&&|2>&1|2>\/dev\/null|>\s*\/dev\/null)\s*/)[0];
|
|
148
|
+
clean = clean.replace(/\x00/g, ';').trim();
|
|
149
|
+
clean = clean.replace(/\s*<<\s*['"]?\w+['"]?\s*$/, '');
|
|
150
|
+
return clean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── inferPhaseStep: 输出永久工具行 ─────────────────────
|
|
154
|
+
|
|
155
|
+
function formatElapsed(indicator) {
|
|
156
|
+
const elapsed = Math.floor((Date.now() - indicator.startTime) / 1000);
|
|
157
|
+
const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
|
158
|
+
const ss = String(elapsed % 60).padStart(2, '0');
|
|
159
|
+
return `${mm}:${ss}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const CODING_TOOLS = /^(write|edit|multiedit|str_replace_editor|strreplace)$/;
|
|
163
|
+
const READ_TOOLS = /^(read|glob|grep|ls)$/;
|
|
164
|
+
|
|
165
|
+
function inferPhaseStep(indicator, toolName, toolInput) {
|
|
166
|
+
const name = (toolName || '').toLowerCase();
|
|
167
|
+
const displayName = toolName || name;
|
|
168
|
+
const pr = indicator.projectRoot || '';
|
|
169
|
+
const cols = termCols();
|
|
170
|
+
|
|
171
|
+
indicator.startTool();
|
|
172
|
+
|
|
173
|
+
let step, target;
|
|
174
|
+
|
|
175
|
+
if (CODING_TOOLS.test(name)) {
|
|
176
|
+
indicator.updatePhase('coding');
|
|
177
|
+
step = displayName;
|
|
178
|
+
target = normalizePath(
|
|
179
|
+
(typeof toolInput === 'object' ? (toolInput.file_path || toolInput.path || '') : ''), pr
|
|
180
|
+
);
|
|
181
|
+
} else if (name === 'bash' || name === 'shell') {
|
|
182
|
+
const cmd = typeof toolInput === 'object' ? (toolInput.command || '') : String(toolInput || '');
|
|
183
|
+
const label = extractBashLabel(cmd);
|
|
184
|
+
step = displayName;
|
|
185
|
+
const url = (label === '网络') ? extractCurlUrl(cmd) : null;
|
|
186
|
+
target = url || stripAbsolutePaths(extractBashCore(cmd), pr);
|
|
187
|
+
if (['测试', '执行'].includes(label)) indicator.updatePhase('coding');
|
|
188
|
+
} else if (READ_TOOLS.test(name)) {
|
|
189
|
+
indicator.updatePhase('thinking');
|
|
190
|
+
step = displayName;
|
|
191
|
+
target = extractTarget(toolInput, pr);
|
|
192
|
+
} else if (name === 'task') {
|
|
193
|
+
indicator.updatePhase('thinking');
|
|
194
|
+
step = displayName;
|
|
195
|
+
target = '';
|
|
196
|
+
} else if (name === 'websearch' || name === 'webfetch') {
|
|
197
|
+
indicator.updatePhase('thinking');
|
|
198
|
+
step = displayName;
|
|
199
|
+
target = '';
|
|
200
|
+
} else if (name.startsWith('mcp__')) {
|
|
201
|
+
indicator.updatePhase('coding');
|
|
202
|
+
step = name.split('__').pop() || displayName;
|
|
203
|
+
target = typeof toolInput === 'object'
|
|
204
|
+
? String(toolInput.url || toolInput.text || toolInput.element || '').slice(0, 60)
|
|
205
|
+
: '';
|
|
206
|
+
} else {
|
|
207
|
+
step = displayName;
|
|
208
|
+
target = '';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const time = localTimestamp();
|
|
212
|
+
const el = formatElapsed(indicator);
|
|
213
|
+
let line = ` ${COLOR.dim}${time}${COLOR.reset} ${COLOR.dim}${el}${COLOR.reset} ${step}`;
|
|
214
|
+
if (target) {
|
|
215
|
+
const maxTarget = Math.max(10, cols - displayWidth(stripAnsi(line)) - 3);
|
|
216
|
+
line += ` ${truncateMiddle(target, maxTarget)}`;
|
|
217
|
+
}
|
|
218
|
+
process.stderr.write(`\r\x1b[K${clampLine(line, cols)}\n`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ─── Terminal width helpers ──────────────────────────────
|
|
222
|
+
|
|
223
|
+
function stripAnsi(str) {
|
|
224
|
+
return str.replace(/\x1b\[[^m]*m/g, '');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isWideChar(cp) {
|
|
228
|
+
return (cp >= 0x4E00 && cp <= 0x9FFF)
|
|
229
|
+
|| (cp >= 0x3400 && cp <= 0x4DBF)
|
|
230
|
+
|| (cp >= 0x3000 && cp <= 0x30FF)
|
|
231
|
+
|| (cp >= 0xF900 && cp <= 0xFAFF)
|
|
232
|
+
|| (cp >= 0xFF01 && cp <= 0xFF60)
|
|
233
|
+
|| (cp >= 0xFFE0 && cp <= 0xFFE6)
|
|
234
|
+
|| (cp >= 0xAC00 && cp <= 0xD7AF)
|
|
235
|
+
|| (cp >= 0x20000 && cp <= 0x2FA1F);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function displayWidth(str) {
|
|
239
|
+
let w = 0;
|
|
240
|
+
for (const ch of str) {
|
|
241
|
+
w += isWideChar(ch.codePointAt(0)) ? 2 : 1;
|
|
242
|
+
}
|
|
243
|
+
return w;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function clampLine(line, cols) {
|
|
247
|
+
const max = cols - 1;
|
|
248
|
+
if (displayWidth(stripAnsi(line)) <= max) return line;
|
|
249
|
+
let w = 0, cut = 0, esc = false;
|
|
250
|
+
for (let i = 0; i < line.length; i++) {
|
|
251
|
+
if (line[i] === '\x1b') esc = true;
|
|
252
|
+
if (esc) { if (line[i] === 'm') esc = false; continue; }
|
|
253
|
+
const cw = isWideChar(line.codePointAt(i)) ? 2 : 1;
|
|
254
|
+
if (w + cw >= max) { cut = i; break; }
|
|
255
|
+
w += cw;
|
|
256
|
+
}
|
|
257
|
+
return line.slice(0, cut) + '…' + COLOR.reset;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = { Indicator, inferPhaseStep };
|