claude-coder 1.9.2 → 1.10.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.
Files changed (81) hide show
  1. package/README.md +236 -214
  2. package/bin/cli.js +170 -155
  3. package/package.json +55 -55
  4. package/recipes/_shared/roles/developer.md +11 -11
  5. package/recipes/_shared/roles/product.md +12 -12
  6. package/recipes/_shared/roles/tester.md +12 -12
  7. package/recipes/_shared/test/report-format.md +86 -86
  8. package/recipes/backend/base.md +27 -27
  9. package/recipes/backend/components/auth.md +18 -18
  10. package/recipes/backend/components/crud-api.md +18 -18
  11. package/recipes/backend/components/file-service.md +15 -15
  12. package/recipes/backend/manifest.json +20 -20
  13. package/recipes/backend/test/api-test.md +25 -25
  14. package/recipes/console/base.md +37 -37
  15. package/recipes/console/components/modal-form.md +20 -20
  16. package/recipes/console/components/pagination.md +17 -17
  17. package/recipes/console/components/search.md +17 -17
  18. package/recipes/console/components/table-list.md +18 -18
  19. package/recipes/console/components/tabs.md +14 -14
  20. package/recipes/console/components/tree.md +15 -15
  21. package/recipes/console/components/upload.md +15 -15
  22. package/recipes/console/manifest.json +24 -24
  23. package/recipes/console/test/crud-e2e.md +47 -47
  24. package/recipes/h5/base.md +26 -26
  25. package/recipes/h5/components/animation.md +11 -11
  26. package/recipes/h5/components/countdown.md +11 -11
  27. package/recipes/h5/components/share.md +11 -11
  28. package/recipes/h5/components/swiper.md +11 -11
  29. package/recipes/h5/manifest.json +21 -21
  30. package/recipes/h5/test/h5-e2e.md +20 -20
  31. package/src/commands/auth.js +420 -420
  32. package/src/commands/setup-modules/helpers.js +100 -100
  33. package/src/commands/setup-modules/index.js +25 -25
  34. package/src/commands/setup-modules/mcp.js +115 -115
  35. package/src/commands/setup-modules/provider.js +260 -260
  36. package/src/commands/setup-modules/safety.js +47 -47
  37. package/src/commands/setup-modules/simplify.js +52 -52
  38. package/src/commands/setup.js +172 -172
  39. package/src/common/assets.js +259 -245
  40. package/src/common/config.js +147 -125
  41. package/src/common/constants.js +55 -55
  42. package/src/common/indicator.js +260 -260
  43. package/src/common/interaction.js +170 -170
  44. package/src/common/logging.js +77 -77
  45. package/src/common/sdk.js +48 -50
  46. package/src/common/tasks.js +88 -88
  47. package/src/common/utils.js +214 -213
  48. package/src/core/coding.js +35 -33
  49. package/src/core/design.js +268 -0
  50. package/src/core/go.js +264 -264
  51. package/src/core/hooks.js +514 -500
  52. package/src/core/init.js +175 -166
  53. package/src/core/plan.js +194 -188
  54. package/src/core/prompts.js +292 -247
  55. package/src/core/repair.js +36 -36
  56. package/src/core/runner.js +471 -471
  57. package/src/core/scan.js +94 -93
  58. package/src/core/session.js +294 -280
  59. package/src/core/simplify.js +76 -74
  60. package/src/core/state.js +120 -105
  61. package/src/index.js +80 -76
  62. package/templates/{codingSystem.md → coding/system.md} +65 -65
  63. package/templates/{codingUser.md → coding/user.md} +18 -17
  64. package/templates/design/base.md +103 -0
  65. package/templates/design/fixSystem.md +71 -0
  66. package/templates/design/fixUser.md +3 -0
  67. package/templates/design/init.md +304 -0
  68. package/templates/design/system.md +108 -0
  69. package/templates/design/user.md +11 -0
  70. package/templates/{goSystem.md → go/system.md} +130 -130
  71. package/templates/{bash-process.md → other/bash-process.md} +12 -12
  72. package/templates/{coreProtocol.md → other/coreProtocol.md} +30 -29
  73. package/templates/{guidance.json → other/guidance.json} +72 -72
  74. package/templates/{requirements.example.md → other/requirements.example.md} +57 -57
  75. package/templates/{test_rule.md → other/test_rule.md} +192 -194
  76. package/templates/{web-testing.md → other/web-testing.md} +17 -17
  77. package/templates/{planSystem.md → plan/system.md} +78 -78
  78. package/templates/{planUser.md → plan/user.md} +10 -9
  79. package/templates/{scanSystem.md → scan/system.md} +120 -120
  80. package/templates/{scanUser.md → scan/user.md} +10 -10
  81. package/types/index.d.ts +217 -217
@@ -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[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 };
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 };