claude-coder 1.8.4 → 1.9.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 +59 -12
- package/bin/cli.js +20 -37
- package/package.json +3 -1
- package/src/commands/auth.js +87 -15
- package/src/commands/setup-modules/helpers.js +4 -3
- package/src/commands/setup-modules/mcp.js +44 -24
- package/src/commands/setup-modules/safety.js +1 -15
- package/src/commands/setup.js +8 -8
- package/src/common/assets.js +10 -1
- package/src/common/config.js +2 -2
- package/src/common/indicator.js +158 -120
- package/src/common/utils.js +60 -8
- package/src/core/coding.js +16 -38
- package/src/core/go.js +31 -77
- package/src/core/hooks.js +56 -89
- package/src/core/init.js +94 -100
- package/src/core/plan.js +85 -223
- package/src/core/prompts.js +36 -16
- package/src/core/repair.js +7 -17
- package/src/core/runner.js +306 -43
- package/src/core/scan.js +38 -34
- package/src/core/session.js +253 -39
- package/src/core/simplify.js +45 -24
- package/src/core/state.js +105 -0
- package/src/index.js +76 -0
- package/templates/codingSystem.md +2 -2
- package/templates/codingUser.md +1 -1
- package/templates/guidance.json +22 -3
- package/templates/planSystem.md +2 -2
- package/templates/scanSystem.md +3 -3
- package/templates/scanUser.md +1 -1
- package/templates/web-testing.md +17 -0
- package/types/index.d.ts +217 -0
- package/src/core/context.js +0 -117
- package/src/core/harness.js +0 -484
- package/src/core/query.js +0 -50
- package/templates/playwright.md +0 -17
package/src/commands/setup.js
CHANGED
|
@@ -20,7 +20,7 @@ const {
|
|
|
20
20
|
} = require('./setup-modules');
|
|
21
21
|
|
|
22
22
|
const PRESERVED_KEYS = [
|
|
23
|
-
'SESSION_STALL_TIMEOUT',
|
|
23
|
+
'SESSION_STALL_TIMEOUT',
|
|
24
24
|
'SESSION_MAX_TURNS', 'SIMPLIFY_INTERVAL', 'SIMPLIFY_COMMITS',
|
|
25
25
|
];
|
|
26
26
|
|
|
@@ -61,10 +61,10 @@ async function setup() {
|
|
|
61
61
|
writeConfig(envPath, configResult.lines);
|
|
62
62
|
ensureGitignore();
|
|
63
63
|
|
|
64
|
-
if (mcpConfig.
|
|
64
|
+
if (mcpConfig.tool) {
|
|
65
65
|
const { updateMcpConfig } = require('./auth');
|
|
66
66
|
const mcpPath = assets.path('mcpConfig');
|
|
67
|
-
updateMcpConfig(mcpPath, mcpConfig.mode);
|
|
67
|
+
updateMcpConfig(mcpPath, mcpConfig.tool, mcpConfig.mode);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
console.log('');
|
|
@@ -98,7 +98,7 @@ async function setup() {
|
|
|
98
98
|
console.log('');
|
|
99
99
|
console.log(' 1) 切换模型提供商');
|
|
100
100
|
console.log(' 2) 更新 API Key');
|
|
101
|
-
console.log(' 3)
|
|
101
|
+
console.log(' 3) 配置浏览器测试工具');
|
|
102
102
|
console.log(' 4) 配置安全限制');
|
|
103
103
|
console.log(' 5) 配置自动审查');
|
|
104
104
|
console.log(' 6) 完全重新配置');
|
|
@@ -119,8 +119,8 @@ async function setup() {
|
|
|
119
119
|
const configResult = await selectProvider(rl, existing);
|
|
120
120
|
preserveSafetyConfig(configResult.lines, existing);
|
|
121
121
|
appendMcpConfig(configResult.lines, {
|
|
122
|
-
|
|
123
|
-
mode: existing.
|
|
122
|
+
tool: existing.WEB_TEST_TOOL || '',
|
|
123
|
+
mode: existing.WEB_TEST_MODE || '',
|
|
124
124
|
});
|
|
125
125
|
writeConfig(envPath, configResult.lines);
|
|
126
126
|
log('ok', `已切换到: ${configResult.summary}`);
|
|
@@ -150,10 +150,10 @@ async function setup() {
|
|
|
150
150
|
appendMcpConfig(configResult.lines, mcpConfig);
|
|
151
151
|
writeConfig(envPath, configResult.lines);
|
|
152
152
|
|
|
153
|
-
if (mcpConfig.
|
|
153
|
+
if (mcpConfig.tool) {
|
|
154
154
|
const { updateMcpConfig } = require('./auth');
|
|
155
155
|
const mcpPath = assets.path('mcpConfig');
|
|
156
|
-
updateMcpConfig(mcpPath, mcpConfig.mode);
|
|
156
|
+
updateMcpConfig(mcpPath, mcpConfig.tool, mcpConfig.mode);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
log('ok', '配置已更新');
|
package/src/common/assets.js
CHANGED
|
@@ -26,7 +26,7 @@ const REGISTRY = new Map([
|
|
|
26
26
|
// Other Templates
|
|
27
27
|
['testRule', { file: 'test_rule.md', kind: 'template' }],
|
|
28
28
|
['guidance', { file: 'guidance.json', kind: 'template' }],
|
|
29
|
-
['
|
|
29
|
+
['webTesting', { file: 'web-testing.md', kind: 'template' }],
|
|
30
30
|
['bashProcess', { file: 'bash-process.md', kind: 'template' }],
|
|
31
31
|
['requirements', { file: 'requirements.example.md', kind: 'template' }],
|
|
32
32
|
|
|
@@ -226,6 +226,15 @@ class AssetManager {
|
|
|
226
226
|
return deployed;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
recipesDir() {
|
|
230
|
+
this._ensureInit();
|
|
231
|
+
const projectRecipes = path.join(this.loopDir, 'recipes');
|
|
232
|
+
if (fs.existsSync(projectRecipes) && fs.readdirSync(projectRecipes).length > 0) {
|
|
233
|
+
return projectRecipes;
|
|
234
|
+
}
|
|
235
|
+
return BUNDLED_RECIPES_DIR;
|
|
236
|
+
}
|
|
237
|
+
|
|
229
238
|
clearCache() {
|
|
230
239
|
this.cache.clear();
|
|
231
240
|
}
|
package/src/common/config.js
CHANGED
|
@@ -54,8 +54,8 @@ function loadConfig() {
|
|
|
54
54
|
authToken: env.ANTHROPIC_AUTH_TOKEN || '',
|
|
55
55
|
model: env.ANTHROPIC_MODEL || '',
|
|
56
56
|
timeoutMs: parseInt(env.API_TIMEOUT_MS, 10) || 3000000,
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
webTestTool: env.WEB_TEST_TOOL || '',
|
|
58
|
+
webTestMode: env.WEB_TEST_MODE || 'persistent',
|
|
59
59
|
disableNonessential: env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC || '',
|
|
60
60
|
effortLevel: env.CLAUDE_CODE_EFFORT_LEVEL || '',
|
|
61
61
|
defaultOpus: env.ANTHROPIC_DEFAULT_OPUS_MODEL || '',
|
package/src/common/indicator.js
CHANGED
|
@@ -1,70 +1,52 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { COLOR } = require('./config');
|
|
4
|
-
const { localTimestamp,
|
|
4
|
+
const { localTimestamp, truncateMiddle } = require('./utils');
|
|
5
5
|
|
|
6
6
|
const SPINNERS = ['⠋', '⠙', '⠸', '⠴', '⠦', '⠇'];
|
|
7
7
|
|
|
8
|
+
function termCols() {
|
|
9
|
+
return process.stderr.columns
|
|
10
|
+
|| process.stdout.columns
|
|
11
|
+
|| parseInt(process.env.COLUMNS, 10)
|
|
12
|
+
|| 70;
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
class Indicator {
|
|
9
16
|
constructor() {
|
|
10
17
|
this.phase = 'thinking';
|
|
11
|
-
this.step = '';
|
|
12
|
-
this.toolTarget = '';
|
|
13
18
|
this.spinnerIndex = 0;
|
|
14
19
|
this.timer = null;
|
|
15
|
-
this.lastActivity = '';
|
|
16
|
-
this.lastToolTime = Date.now();
|
|
17
20
|
this.lastActivityTime = Date.now();
|
|
18
21
|
this.sessionNum = 0;
|
|
19
22
|
this.startTime = Date.now();
|
|
20
23
|
this.stallTimeoutMin = 30;
|
|
21
|
-
this.completionTimeoutMin = null;
|
|
22
24
|
this.toolRunning = false;
|
|
23
25
|
this.toolStartTime = 0;
|
|
24
|
-
this.currentToolName = '';
|
|
25
26
|
this._paused = false;
|
|
27
|
+
this.projectRoot = '';
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
start(sessionNum, stallTimeoutMin) {
|
|
30
|
+
start(sessionNum, stallTimeoutMin, projectRoot) {
|
|
29
31
|
this.sessionNum = sessionNum;
|
|
30
32
|
this.startTime = Date.now();
|
|
31
33
|
this.lastActivityTime = Date.now();
|
|
32
34
|
if (stallTimeoutMin > 0) this.stallTimeoutMin = stallTimeoutMin;
|
|
35
|
+
if (projectRoot) this.projectRoot = projectRoot;
|
|
33
36
|
this.timer = setInterval(() => this._render(), 1000);
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
stop() {
|
|
37
|
-
if (this.timer) {
|
|
38
|
-
clearInterval(this.timer);
|
|
39
|
-
this.timer = null;
|
|
40
|
-
}
|
|
40
|
+
if (this.timer) { clearInterval(this.timer); this.timer = null; }
|
|
41
41
|
process.stderr.write('\r\x1b[K');
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
updatePhase(phase) {
|
|
45
|
-
|
|
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
|
-
}
|
|
44
|
+
updatePhase(phase) { this.phase = phase; }
|
|
45
|
+
updateActivity() { this.lastActivityTime = Date.now(); }
|
|
63
46
|
|
|
64
|
-
startTool(
|
|
47
|
+
startTool() {
|
|
65
48
|
this.toolRunning = true;
|
|
66
49
|
this.toolStartTime = Date.now();
|
|
67
|
-
this.currentToolName = name;
|
|
68
50
|
this.lastActivityTime = Date.now();
|
|
69
51
|
}
|
|
70
52
|
|
|
@@ -77,13 +59,13 @@ class Indicator {
|
|
|
77
59
|
pauseRendering() { this._paused = true; }
|
|
78
60
|
resumeRendering() { this._paused = false; }
|
|
79
61
|
|
|
80
|
-
|
|
81
|
-
|
|
62
|
+
_render() {
|
|
63
|
+
if (this._paused) return;
|
|
64
|
+
this.spinnerIndex++;
|
|
82
65
|
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
|
83
66
|
const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
|
84
67
|
const ss = String(elapsed % 60).padStart(2, '0');
|
|
85
68
|
const spinner = SPINNERS[this.spinnerIndex % SPINNERS.length];
|
|
86
|
-
|
|
87
69
|
const phaseLabel = this.phase === 'thinking'
|
|
88
70
|
? `${COLOR.yellow}思考中${COLOR.reset}`
|
|
89
71
|
: `${COLOR.green}编码中${COLOR.reset}`;
|
|
@@ -91,132 +73,188 @@ class Indicator {
|
|
|
91
73
|
const idleMs = Date.now() - this.lastActivityTime;
|
|
92
74
|
const idleMin = Math.floor(idleMs / 60000);
|
|
93
75
|
|
|
94
|
-
let line = `${spinner}
|
|
76
|
+
let line = `${spinner} S${this.sessionNum} ${mm}:${ss} ${phaseLabel}`;
|
|
95
77
|
if (idleMin >= 2) {
|
|
96
78
|
if (this.toolRunning) {
|
|
97
|
-
const
|
|
98
|
-
|
|
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}`;
|
|
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}`;
|
|
103
81
|
} else {
|
|
104
|
-
line += `
|
|
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}`;
|
|
82
|
+
line += ` ${COLOR.red}${idleMin}分无响应${COLOR.reset}`;
|
|
116
83
|
}
|
|
117
84
|
}
|
|
118
|
-
|
|
85
|
+
process.stderr.write(`\r\x1b[K${line}`);
|
|
119
86
|
}
|
|
87
|
+
}
|
|
120
88
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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;
|
|
125
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;
|
|
126
101
|
}
|
|
127
102
|
|
|
128
|
-
function
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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;
|
|
134
113
|
}
|
|
135
114
|
|
|
136
|
-
function
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
if (cmd
|
|
142
|
-
|
|
143
|
-
return
|
|
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 '';
|
|
144
124
|
}
|
|
145
125
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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 '执行';
|
|
149
136
|
}
|
|
150
137
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
function extractBashTarget(cmd) {
|
|
156
|
-
// 移除开头的 cd xxx && 部分
|
|
157
|
-
let clean = cmd.replace(/^(?:cd\s+\S+\s*&&\s*)+/g, '').trim();
|
|
138
|
+
function extractCurlUrl(cmd) {
|
|
139
|
+
const m = cmd.match(/https?:\/\/\S+/);
|
|
140
|
+
return m ? m[0].replace(/['";)}\]>]+$/, '') : null;
|
|
141
|
+
}
|
|
158
142
|
|
|
159
|
-
|
|
160
|
-
|
|
143
|
+
function extractBashCore(cmd) {
|
|
144
|
+
let clean = cmd.replace(/^(?:(?:cd|source|export)\s+\S+\s*&&\s*)+/g, '').trim();
|
|
161
145
|
clean = clean.replace(/"[^"]*"/g, m => m.replace(/[;|&]/g, '\x00'));
|
|
162
146
|
clean = clean.replace(/'[^']*'/g, m => m.replace(/[;|&]/g, '\x00'));
|
|
163
|
-
|
|
164
|
-
// 分割并取第一部分
|
|
165
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: 输出永久工具行 ─────────────────────
|
|
166
154
|
|
|
167
|
-
|
|
168
|
-
|
|
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}`;
|
|
169
160
|
}
|
|
170
161
|
|
|
162
|
+
const CODING_TOOLS = /^(write|edit|multiedit|str_replace_editor|strreplace)$/;
|
|
163
|
+
const READ_TOOLS = /^(read|glob|grep|ls)$/;
|
|
164
|
+
|
|
171
165
|
function inferPhaseStep(indicator, toolName, toolInput) {
|
|
172
166
|
const name = (toolName || '').toLowerCase();
|
|
167
|
+
const displayName = toolName || name;
|
|
168
|
+
const pr = indicator.projectRoot || '';
|
|
169
|
+
const cols = termCols();
|
|
170
|
+
|
|
171
|
+
indicator.startTool();
|
|
173
172
|
|
|
174
|
-
|
|
173
|
+
let step, target;
|
|
175
174
|
|
|
176
|
-
if (name
|
|
175
|
+
if (CODING_TOOLS.test(name)) {
|
|
177
176
|
indicator.updatePhase('coding');
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
step = displayName;
|
|
178
|
+
target = normalizePath(
|
|
179
|
+
(typeof toolInput === 'object' ? (toolInput.file_path || toolInput.path || '') : ''), pr
|
|
180
|
+
);
|
|
180
181
|
} else if (name === 'bash' || name === 'shell') {
|
|
181
182
|
const cmd = typeof toolInput === 'object' ? (toolInput.command || '') : String(toolInput || '');
|
|
182
183
|
const label = extractBashLabel(cmd);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
} else if (name === 'read' || name === 'glob' || name === 'grep' || name === 'ls') {
|
|
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
189
|
indicator.updatePhase('thinking');
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
step = displayName;
|
|
191
|
+
target = extractTarget(toolInput, pr);
|
|
192
192
|
} else if (name === 'task') {
|
|
193
193
|
indicator.updatePhase('thinking');
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
step = displayName;
|
|
195
|
+
target = '';
|
|
196
196
|
} else if (name === 'websearch' || name === 'webfetch') {
|
|
197
197
|
indicator.updatePhase('thinking');
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
step = displayName;
|
|
199
|
+
target = '';
|
|
200
200
|
} else if (name.startsWith('mcp__')) {
|
|
201
201
|
indicator.updatePhase('coding');
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
step = name.split('__').pop() || displayName;
|
|
203
|
+
target = typeof toolInput === 'object'
|
|
204
|
+
? String(toolInput.url || toolInput.text || toolInput.element || '').slice(0, 60)
|
|
205
|
+
: '';
|
|
205
206
|
} else {
|
|
206
|
-
|
|
207
|
-
|
|
207
|
+
step = displayName;
|
|
208
|
+
target = '';
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
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;
|
|
218
256
|
}
|
|
219
|
-
|
|
257
|
+
return line.slice(0, cut) + '…' + COLOR.reset;
|
|
220
258
|
}
|
|
221
259
|
|
|
222
260
|
module.exports = { Indicator, inferPhaseStep };
|
package/src/common/utils.js
CHANGED
|
@@ -56,6 +56,17 @@ function truncatePath(path, maxLen) {
|
|
|
56
56
|
return truncatedDir + '/' + fileName;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* 命令字符串截断:保留头部,超长时截断
|
|
61
|
+
* @param {string} cmd - 命令字符串
|
|
62
|
+
* @param {number} maxLen - 最大长度
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
function truncateCommand(cmd, maxLen) {
|
|
66
|
+
if (!cmd || cmd.length <= maxLen) return cmd || '';
|
|
67
|
+
return cmd.slice(0, maxLen - 1) + '…';
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
// ─────────────────────────────────────────────────────────────
|
|
60
71
|
// Git 工具
|
|
61
72
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -112,12 +123,17 @@ function appendGitignore(projectRoot, entry) {
|
|
|
112
123
|
}
|
|
113
124
|
|
|
114
125
|
/**
|
|
115
|
-
* 确保 .gitignore 包含 claude-coder
|
|
126
|
+
* 确保 .gitignore 包含 claude-coder 的忽略规则
|
|
127
|
+
* 使用通配符忽略整个目录,仅白名单放行需要版本控制的文件
|
|
116
128
|
* @param {string} projectRoot - 项目根目录
|
|
117
129
|
* @returns {boolean} 是否有新增
|
|
118
130
|
*/
|
|
119
131
|
function ensureGitignore(projectRoot) {
|
|
120
|
-
const patterns = [
|
|
132
|
+
const patterns = [
|
|
133
|
+
'.claude-coder/*',
|
|
134
|
+
'!.claude-coder/tasks.json',
|
|
135
|
+
'!.claude-coder/project_profile.json',
|
|
136
|
+
];
|
|
121
137
|
let added = false;
|
|
122
138
|
for (const p of patterns) {
|
|
123
139
|
if (appendGitignore(projectRoot, p)) added = true;
|
|
@@ -129,18 +145,51 @@ function ensureGitignore(projectRoot) {
|
|
|
129
145
|
// 进程工具
|
|
130
146
|
// ─────────────────────────────────────────────────────────────
|
|
131
147
|
|
|
132
|
-
/**
|
|
133
|
-
* 休眠
|
|
134
|
-
* @param {number} ms - 毫秒
|
|
135
|
-
* @returns {Promise<void>}
|
|
136
|
-
*/
|
|
137
148
|
function sleep(ms) {
|
|
138
149
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
139
150
|
}
|
|
140
151
|
|
|
152
|
+
// ─────────────────────────────────────────────────────────────
|
|
153
|
+
// 项目服务管理
|
|
154
|
+
// ─────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
function tryPush(projectRoot) {
|
|
157
|
+
const { log } = require('./config');
|
|
158
|
+
try {
|
|
159
|
+
const remotes = execSync('git remote', { cwd: projectRoot, encoding: 'utf8' }).trim();
|
|
160
|
+
if (!remotes) return;
|
|
161
|
+
log('info', '正在推送代码...');
|
|
162
|
+
execSync('git push', { cwd: projectRoot, stdio: 'inherit' });
|
|
163
|
+
log('ok', '推送成功');
|
|
164
|
+
} catch {
|
|
165
|
+
log('warn', '推送失败 (请检查网络或权限),继续执行...');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function killServices(projectRoot) {
|
|
170
|
+
const { log } = require('./config');
|
|
171
|
+
const { assets } = require('./assets');
|
|
172
|
+
const profile = assets.readJson('profile', null);
|
|
173
|
+
if (!profile) return;
|
|
174
|
+
const ports = (profile.services || []).map(s => s.port).filter(Boolean);
|
|
175
|
+
if (ports.length === 0) return;
|
|
176
|
+
|
|
177
|
+
for (const port of ports) {
|
|
178
|
+
try {
|
|
179
|
+
if (process.platform === 'win32') {
|
|
180
|
+
const out = execSync(`netstat -ano | findstr :${port} | findstr LISTENING`, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
181
|
+
const pids = [...new Set(out.split('\n').map(l => l.trim().split(/\s+/).pop()).filter(Boolean))];
|
|
182
|
+
for (const pid of pids) { try { execSync(`taskkill /F /T /PID ${pid}`, { stdio: 'pipe' }); } catch { /* ignore */ } }
|
|
183
|
+
} else {
|
|
184
|
+
execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null`, { stdio: 'pipe' });
|
|
185
|
+
}
|
|
186
|
+
} catch { /* no process on port */ }
|
|
187
|
+
}
|
|
188
|
+
log('info', `已停止端口 ${ports.join(', ')} 上的服务`);
|
|
189
|
+
}
|
|
141
190
|
|
|
142
191
|
// ─────────────────────────────────────────────────────────────
|
|
143
|
-
// 日志工具
|
|
192
|
+
// 日志工具
|
|
144
193
|
// ─────────────────────────────────────────────────────────────
|
|
145
194
|
function localTimestamp() {
|
|
146
195
|
const d = new Date();
|
|
@@ -153,10 +202,13 @@ function localTimestamp() {
|
|
|
153
202
|
module.exports = {
|
|
154
203
|
truncateMiddle,
|
|
155
204
|
truncatePath,
|
|
205
|
+
truncateCommand,
|
|
156
206
|
getGitHead,
|
|
157
207
|
isGitRepo,
|
|
158
208
|
appendGitignore,
|
|
159
209
|
ensureGitignore,
|
|
160
210
|
sleep,
|
|
211
|
+
tryPush,
|
|
212
|
+
killServices,
|
|
161
213
|
localTimestamp,
|
|
162
214
|
};
|
package/src/core/coding.js
CHANGED
|
@@ -1,55 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const {
|
|
4
|
-
const {
|
|
5
|
-
const {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* 内部:运行编码 Session
|
|
11
|
-
*/
|
|
12
|
-
async function runCodingSession(sessionNum, opts = {}) {
|
|
13
|
-
const taskId = opts.taskId || "unknown";
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { buildSystemPrompt, buildCodingContext } = require('./prompts');
|
|
4
|
+
const { Session } = require('./session');
|
|
5
|
+
const { log } = require('../common/config');
|
|
6
|
+
|
|
7
|
+
async function executeCoding(config, sessionNum, opts = {}) {
|
|
8
|
+
const taskId = opts.taskId || 'unknown';
|
|
14
9
|
const dateStr = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
15
10
|
|
|
16
|
-
return
|
|
17
|
-
opts,
|
|
11
|
+
return Session.run('coding', config, {
|
|
18
12
|
sessionNum,
|
|
19
13
|
logFileName: `${taskId}_session_${sessionNum}_${dateStr}.log`,
|
|
20
14
|
label: `coding task=${taskId}`,
|
|
21
15
|
|
|
22
|
-
async execute(
|
|
16
|
+
async execute(session) {
|
|
23
17
|
const prompt = buildCodingContext(sessionNum, opts);
|
|
24
|
-
const queryOpts = buildQueryOptions(
|
|
18
|
+
const queryOpts = session.buildQueryOptions(opts);
|
|
25
19
|
queryOpts.systemPrompt = buildSystemPrompt('coding');
|
|
26
|
-
queryOpts.hooks = ctx.hooks;
|
|
27
|
-
queryOpts.abortController = ctx.abortController;
|
|
28
20
|
queryOpts.disallowedTools = ['askUserQuestion'];
|
|
29
21
|
|
|
30
|
-
const
|
|
31
|
-
const result = extractResult(collected);
|
|
32
|
-
const subtype = result?.subtype || "unknown";
|
|
22
|
+
const { subtype, cost, usage } = await session.runQuery(prompt, queryOpts);
|
|
33
23
|
|
|
34
|
-
if (subtype !==
|
|
35
|
-
log(
|
|
36
|
-
"warn",
|
|
37
|
-
`session 结束原因: ${subtype} (turns: ${result?.num_turns ?? "?"})`,
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
if (ctx.logStream.writable) {
|
|
41
|
-
ctx.logStream.write(
|
|
42
|
-
`[${new Date().toISOString()}] SESSION_END subtype=${subtype} turns=${result?.num_turns ?? "?"} cost=${result?.total_cost_usd ?? "?"}\n`,
|
|
43
|
-
);
|
|
24
|
+
if (subtype && subtype !== 'success' && subtype !== 'unknown') {
|
|
25
|
+
log('warn', `session 结束原因: ${subtype}`);
|
|
44
26
|
}
|
|
45
27
|
|
|
46
|
-
return {
|
|
47
|
-
cost: result?.total_cost_usd ?? null,
|
|
48
|
-
tokenUsage: result?.usage ?? null,
|
|
49
|
-
subtype,
|
|
50
|
-
};
|
|
28
|
+
return { cost, tokenUsage: usage, subtype: subtype || 'unknown' };
|
|
51
29
|
},
|
|
52
30
|
});
|
|
53
31
|
}
|
|
54
32
|
|
|
55
|
-
module.exports = {
|
|
33
|
+
module.exports = { executeCoding };
|