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.
@@ -20,7 +20,7 @@ const {
20
20
  } = require('./setup-modules');
21
21
 
22
22
  const PRESERVED_KEYS = [
23
- 'SESSION_STALL_TIMEOUT', 'SESSION_COMPLETION_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.enabled && mcpConfig.mode) {
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) 配置 MCP');
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
- enabled: existing.MCP_PLAYWRIGHT === 'true',
123
- mode: existing.MCP_PLAYWRIGHT_MODE || null,
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.enabled && mcpConfig.mode) {
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', '配置已更新');
@@ -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
- ['playwright', { file: 'playwright.md', kind: 'template' }],
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
  }
@@ -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
- mcpPlaywright: env.MCP_PLAYWRIGHT === 'true',
58
- playwrightMode: env.MCP_PLAYWRIGHT_MODE || 'persistent',
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 || '',
@@ -1,70 +1,52 @@
1
1
  'use strict';
2
2
 
3
3
  const { COLOR } = require('./config');
4
- const { localTimestamp, truncatePath } = require('./utils');
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
- this.phase = phase;
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(name) {
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
- getStatusLine() {
81
- const clock = localTimestamp();
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} [Session ${this.sessionNum}] ${clock} ${phaseLabel} ${mm}:${ss}`;
76
+ let line = `${spinner} S${this.sessionNum} ${mm}:${ss} ${phaseLabel}`;
95
77
  if (idleMin >= 2) {
96
78
  if (this.toolRunning) {
97
- const toolSec = Math.floor((Date.now() - this.toolStartTime) / 1000);
98
- const toolMm = Math.floor(toolSec / 60);
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 += ` | ${COLOR.red}${idleMin}分无响应(等待模型响应, ${this.stallTimeoutMin}分钟超时自动中断)${COLOR.reset}`;
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
- return line;
85
+ process.stderr.write(`\r\x1b[K${line}`);
119
86
  }
87
+ }
120
88
 
121
- _render() {
122
- if (this._paused) return;
123
- this.spinnerIndex++;
124
- process.stderr.write(`\r\x1b[K${this.getStatusLine()}`);
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 extractFileTarget(toolInput) {
129
- const raw = typeof toolInput === 'object'
130
- ? (toolInput.file_path || toolInput.path || '')
131
- : '';
132
- if (!raw) return '';
133
- return raw.split('/').slice(-2).join('/');
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 extractBashLabel(cmd) {
137
- if (cmd.includes('git ')) return 'Git 操作';
138
- if (cmd.includes('npm ') || cmd.includes('pip ') || cmd.includes('pnpm ') || cmd.includes('yarn ')) return '安装依赖';
139
- if (/\b(sleep|Start-Sleep|timeout\s+\/t)\b/i.test(cmd)) return '等待就绪';
140
- if (cmd.includes('curl')) return '网络请求';
141
- if (cmd.includes('pytest') || cmd.includes('jest') || /\btest\b/.test(cmd)) return '测试验证';
142
- if (cmd.includes('python ') || cmd.includes('node ')) return '执行脚本';
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
- function extractMcpTarget(toolInput) {
147
- if (!toolInput || typeof toolInput !== 'object') return '';
148
- return String(toolInput.url || toolInput.text || toolInput.element || '').slice(0, 60);
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
- * 提取 Bash 命令的主体部分(移除管道、重定向等)
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
- const unescape = (s) => s.replace(/\x00/g, ';');
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
- return unescape(clean).trim();
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
- indicator.startTool(toolName);
173
+ let step, target;
175
174
 
176
- if (name === 'write' || name === 'edit' || name === 'multiedit' || name === 'str_replace_editor' || name === 'strreplace') {
175
+ if (CODING_TOOLS.test(name)) {
177
176
  indicator.updatePhase('coding');
178
- indicator.updateStep('编辑文件');
179
- indicator.toolTarget = extractFileTarget(toolInput);
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
- indicator.updateStep(label);
184
- indicator.toolTarget = extractBashTarget(cmd);
185
- if (label === '测试验证' || label === '执行脚本' || label === '执行命令') {
186
- indicator.updatePhase('coding');
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
- indicator.updateStep('读取文件');
191
- indicator.toolTarget = extractFileTarget(toolInput);
190
+ step = displayName;
191
+ target = extractTarget(toolInput, pr);
192
192
  } else if (name === 'task') {
193
193
  indicator.updatePhase('thinking');
194
- indicator.updateStep('子 Agent 搜索');
195
- indicator.toolTarget = '';
194
+ step = displayName;
195
+ target = '';
196
196
  } else if (name === 'websearch' || name === 'webfetch') {
197
197
  indicator.updatePhase('thinking');
198
- indicator.updateStep('查阅文档');
199
- indicator.toolTarget = '';
198
+ step = displayName;
199
+ target = '';
200
200
  } else if (name.startsWith('mcp__')) {
201
201
  indicator.updatePhase('coding');
202
- const action = name.split('__').pop() || name;
203
- indicator.updateStep(`浏览器: ${action}`);
204
- indicator.toolTarget = extractMcpTarget(toolInput);
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
- indicator.updateStep('工具调用');
207
- indicator.toolTarget = '';
207
+ step = displayName;
208
+ target = '';
208
209
  }
209
210
 
210
- let summary;
211
- if (typeof toolInput === 'object') {
212
- const target = toolInput.file_path || toolInput.path || '';
213
- const cmd = toolInput.command || '';
214
- const pattern = toolInput.pattern || '';
215
- summary = target || (cmd ? cmd.slice(0, 200) : '') || (pattern ? `pattern: ${pattern}` : JSON.stringify(toolInput).slice(0, 200));
216
- } else {
217
- summary = String(toolInput || '').slice(0, 200);
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
- indicator.appendActivity(toolName, summary);
257
+ return line.slice(0, cut) + '…' + COLOR.reset;
220
258
  }
221
259
 
222
260
  module.exports = { Indicator, inferPhaseStep };
@@ -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 = ['.claude-coder/.env', '.claude-coder/.runtime/'];
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
  };
@@ -1,55 +1,33 @@
1
- "use strict";
2
-
3
- const { runSession } = require("./session");
4
- const { buildQueryOptions } = require("./query");
5
- const { buildSystemPrompt, buildCodingContext } = require("./prompts");
6
- const { extractResult } = require("../common/logging");
7
- const { log } = require("../common/config");
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 runSession("coding", {
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(sdk, ctx) {
16
+ async execute(session) {
23
17
  const prompt = buildCodingContext(sessionNum, opts);
24
- const queryOpts = buildQueryOptions(ctx.config, opts);
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 collected = await ctx.runQuery(sdk, prompt, queryOpts);
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 !== "success" && subtype !== "unknown") {
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 = { runCodingSession };
33
+ module.exports = { executeCoding };