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.
Files changed (74) hide show
  1. package/README.md +214 -214
  2. package/bin/cli.js +155 -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 -362
  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 +245 -245
  40. package/src/common/config.js +125 -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 +50 -50
  46. package/src/common/tasks.js +88 -88
  47. package/src/common/utils.js +213 -213
  48. package/src/core/coding.js +33 -33
  49. package/src/core/go.js +264 -264
  50. package/src/core/hooks.js +500 -500
  51. package/src/core/init.js +166 -165
  52. package/src/core/plan.js +188 -187
  53. package/src/core/prompts.js +247 -247
  54. package/src/core/repair.js +36 -36
  55. package/src/core/runner.js +471 -458
  56. package/src/core/scan.js +93 -93
  57. package/src/core/session.js +280 -271
  58. package/src/core/simplify.js +74 -74
  59. package/src/core/state.js +105 -105
  60. package/src/index.js +76 -76
  61. package/templates/bash-process.md +12 -12
  62. package/templates/codingSystem.md +65 -65
  63. package/templates/codingUser.md +17 -17
  64. package/templates/coreProtocol.md +29 -29
  65. package/templates/goSystem.md +130 -130
  66. package/templates/guidance.json +72 -72
  67. package/templates/planSystem.md +78 -78
  68. package/templates/planUser.md +8 -8
  69. package/templates/requirements.example.md +57 -57
  70. package/templates/scanSystem.md +120 -120
  71. package/templates/scanUser.md +10 -10
  72. package/templates/test_rule.md +194 -194
  73. package/templates/web-testing.md +17 -17
  74. 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.startsWith('/') ? rel.slice(1) : rel;
96
- }
97
- const home = process.env.HOME || '';
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 || '';
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 };