claude-coder 1.6.2 → 1.7.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/src/session.js CHANGED
@@ -1,320 +1,352 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { paths, loadConfig, buildEnvVars, getAllowedTools, log } = require('./config');
6
- const { Indicator } = require('./indicator');
7
- const { createSessionHooks } = require('./hooks');
8
- const { buildSystemPrompt, buildCodingPrompt, buildScanPrompt, buildAddSystemPrompt, buildAddPrompt } = require('./prompts');
9
-
10
- // ── SDK loader (cached, shared across sessions) ──
11
-
12
- let _sdkModule = null;
13
- async function loadSDK() {
14
- if (_sdkModule) return _sdkModule;
15
-
16
- const pkgName = '@anthropic-ai/claude-agent-sdk';
17
- const attempts = [
18
- () => import(pkgName),
19
- () => {
20
- const { createRequire } = require('module');
21
- const resolved = createRequire(__filename).resolve(pkgName);
22
- return import(resolved);
23
- },
24
- () => {
25
- const { execSync } = require('child_process');
26
- const prefix = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
27
- const sdkPath = path.join(prefix, 'lib', 'node_modules', pkgName, 'sdk.mjs');
28
- return import(sdkPath);
29
- },
30
- ];
31
-
32
- for (const attempt of attempts) {
33
- try {
34
- _sdkModule = await attempt();
35
- return _sdkModule;
36
- } catch { /* try next */ }
37
- }
38
-
39
- log('error', `未找到 ${pkgName}`);
40
- log('error', `请先安装:npm install -g ${pkgName}`);
41
- process.exit(1);
42
- }
43
-
44
- // ── Helpers ──
45
-
46
- function applyEnvConfig(config) {
47
- Object.assign(process.env, buildEnvVars(config));
48
- }
49
-
50
- function buildQueryOptions(config, opts = {}) {
51
- const base = {
52
- allowedTools: getAllowedTools(config),
53
- permissionMode: 'bypassPermissions',
54
- allowDangerouslySkipPermissions: true,
55
- cwd: opts.projectRoot || process.cwd(),
56
- env: buildEnvVars(config),
57
- settingSources: ['project'],
58
- };
59
- if (opts.model) base.model = opts.model;
60
- else if (config.model) base.model = config.model;
61
- return base;
62
- }
63
-
64
- function extractResult(messages) {
65
- for (let i = messages.length - 1; i >= 0; i--) {
66
- if (messages[i].type === 'result') return messages[i];
67
- }
68
- return null;
69
- }
70
-
71
- function writeSessionSeparator(logStream, sessionNum, label) {
72
- const sep = '='.repeat(60);
73
- logStream.write(`\n${sep}\n[Session ${sessionNum}] ${label} ${new Date().toISOString()}\n${sep}\n`);
74
- }
75
-
76
- let _lastPrintedStatusKey = '';
77
-
78
- function logMessage(message, logStream, indicator) {
79
- if (message.type === 'assistant' && message.message?.content) {
80
- for (const block of message.message.content) {
81
- if (block.type === 'text' && block.text) {
82
- if (indicator) {
83
- process.stderr.write('\r\x1b[K');
84
- const contentKey = `${indicator.phase}|${indicator.step}|${indicator.toolTarget}`;
85
- if (contentKey !== _lastPrintedStatusKey) {
86
- _lastPrintedStatusKey = contentKey;
87
- const statusLine = indicator.getStatusLine();
88
- if (statusLine) process.stderr.write(statusLine + '\n');
89
- }
90
- }
91
- process.stdout.write(block.text);
92
- if (logStream) logStream.write(block.text);
93
- }
94
- if (block.type === 'tool_use' && logStream) {
95
- logStream.write(`[TOOL_USE] ${block.name}: ${JSON.stringify(block.input).slice(0, 300)}\n`);
96
- }
97
- }
98
- }
99
-
100
- if (message.type === 'tool_result' && logStream) {
101
- const isErr = message.is_error || false;
102
- const content = typeof message.content === 'string'
103
- ? message.content.slice(0, 500)
104
- : JSON.stringify(message.content).slice(0, 500);
105
- if (isErr) {
106
- logStream.write(`[TOOL_ERROR] ${content}\n`);
107
- }
108
- }
109
- }
110
-
111
- // ── Session runners ──
112
-
113
- async function runCodingSession(sessionNum, opts = {}) {
114
- const sdk = await loadSDK();
115
- const config = loadConfig();
116
- applyEnvConfig(config);
117
- const indicator = new Indicator();
118
-
119
- const prompt = buildCodingPrompt(sessionNum, opts);
120
- const systemPrompt = buildSystemPrompt(false);
121
-
122
- const p = paths();
123
- const taskId = opts.taskId || 'unknown';
124
- const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
125
- const logFile = path.join(p.logsDir, `${taskId}_session_${sessionNum}_${dateStr}.log`);
126
- const logStream = fs.createWriteStream(logFile, { flags: 'a' });
127
-
128
- writeSessionSeparator(logStream, sessionNum, `coding task=${taskId}`);
129
-
130
- const stallTimeoutMs = config.stallTimeout * 1000;
131
- const { hooks, cleanup, isStalled } = createSessionHooks(indicator, logStream, {
132
- enableStallDetection: true,
133
- stallTimeoutMs,
134
- enableEditGuard: true,
135
- editThreshold: config.editThreshold,
136
- });
137
-
138
- const stallTimeoutMin = Math.floor(stallTimeoutMs / 60000);
139
- indicator.start(sessionNum, stallTimeoutMin);
140
-
141
- try {
142
- const queryOpts = buildQueryOptions(config, opts);
143
- queryOpts.systemPrompt = systemPrompt;
144
- queryOpts.hooks = hooks;
145
-
146
- const session = sdk.query({ prompt, options: queryOpts });
147
-
148
- const collected = [];
149
- for await (const message of session) {
150
- if (isStalled()) {
151
- log('warn', '停顿超时,中断消息循环');
152
- break;
153
- }
154
- collected.push(message);
155
- logMessage(message, logStream, indicator);
156
- }
157
-
158
- cleanup();
159
- logStream.end();
160
- indicator.stop();
161
-
162
- const result = extractResult(collected);
163
- return {
164
- exitCode: isStalled() ? 2 : 0,
165
- cost: result?.total_cost_usd ?? null,
166
- tokenUsage: result?.usage ?? null,
167
- logFile,
168
- stalled: isStalled(),
169
- };
170
- } catch (err) {
171
- cleanup();
172
- logStream.end();
173
- indicator.stop();
174
- log('error', `Claude SDK 错误: ${err.message}`);
175
- return {
176
- exitCode: 1,
177
- cost: null,
178
- tokenUsage: null,
179
- logFile,
180
- error: err.message,
181
- };
182
- }
183
- }
184
-
185
- async function runScanSession(requirement, opts = {}) {
186
- const sdk = await loadSDK();
187
- const config = loadConfig();
188
- applyEnvConfig(config);
189
- const indicator = new Indicator();
190
-
191
- const projectType = hasCodeFiles(opts.projectRoot || process.cwd()) ? 'existing' : 'new';
192
- const prompt = buildScanPrompt(projectType, requirement);
193
- const systemPrompt = buildSystemPrompt(true);
194
-
195
- const p = paths();
196
- const logFile = path.join(p.logsDir, `scan_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.log`);
197
- const logStream = fs.createWriteStream(logFile, { flags: 'a' });
198
-
199
- writeSessionSeparator(logStream, 0, `scan (${projectType})`);
200
-
201
- const stallTimeoutMs = config.stallTimeout * 1000;
202
- const { hooks, cleanup, isStalled } = createSessionHooks(indicator, logStream, {
203
- enableStallDetection: true,
204
- stallTimeoutMs,
205
- });
206
-
207
- indicator.start(0, Math.floor(stallTimeoutMs / 60000));
208
- log('info', `正在调用 Claude Code 执行项目扫描(${projectType}项目)...`);
209
-
210
- try {
211
- const queryOpts = buildQueryOptions(config, opts);
212
- queryOpts.systemPrompt = systemPrompt;
213
- queryOpts.hooks = hooks;
214
-
215
- const session = sdk.query({ prompt, options: queryOpts });
216
-
217
- const collected = [];
218
- for await (const message of session) {
219
- if (isStalled()) {
220
- log('warn', '扫描停顿超时,中断');
221
- break;
222
- }
223
- collected.push(message);
224
- logMessage(message, logStream, indicator);
225
- }
226
-
227
- cleanup();
228
- logStream.end();
229
- indicator.stop();
230
-
231
- const result = extractResult(collected);
232
- return {
233
- exitCode: isStalled() ? 2 : 0,
234
- cost: result?.total_cost_usd ?? null,
235
- logFile,
236
- stalled: isStalled(),
237
- };
238
- } catch (err) {
239
- cleanup();
240
- logStream.end();
241
- indicator.stop();
242
- log('error', `扫描失败: ${err.message}`);
243
- return { exitCode: 1, cost: null, logFile, error: err.message };
244
- }
245
- }
246
-
247
- async function runAddSession(instruction, opts = {}) {
248
- const sdk = await loadSDK();
249
- const config = loadConfig();
250
- applyEnvConfig(config);
251
- const indicator = new Indicator();
252
-
253
- const systemPrompt = buildAddSystemPrompt();
254
- const prompt = buildAddPrompt(instruction);
255
-
256
- const p = paths();
257
- const logFile = path.join(p.logsDir, `add_tasks_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.log`);
258
- const logStream = fs.createWriteStream(logFile, { flags: 'a' });
259
-
260
- writeSessionSeparator(logStream, 0, 'add tasks');
261
-
262
- const stallTimeoutMs = config.stallTimeout * 1000;
263
- const { hooks, cleanup, isStalled } = createSessionHooks(indicator, logStream, {
264
- enableStallDetection: true,
265
- stallTimeoutMs,
266
- });
267
-
268
- indicator.start(0, Math.floor(stallTimeoutMs / 60000));
269
- log('info', '正在追加任务...');
270
-
271
- try {
272
- const queryOpts = buildQueryOptions(config, opts);
273
- queryOpts.systemPrompt = systemPrompt;
274
- queryOpts.hooks = hooks;
275
-
276
- const session = sdk.query({ prompt, options: queryOpts });
277
-
278
- for await (const message of session) {
279
- if (isStalled()) {
280
- log('warn', '追加任务停顿超时,中断');
281
- break;
282
- }
283
- logMessage(message, logStream, indicator);
284
- }
285
-
286
- cleanup();
287
- logStream.end();
288
- indicator.stop();
289
- log('ok', '任务追加完成');
290
- } catch (err) {
291
- cleanup();
292
- logStream.end();
293
- indicator.stop();
294
- log('error', `任务追加失败: ${err.message}`);
295
- }
296
- }
297
-
298
- function hasCodeFiles(projectRoot) {
299
- const markers = [
300
- 'package.json', 'pyproject.toml', 'requirements.txt', 'setup.py',
301
- 'Cargo.toml', 'go.mod', 'pom.xml', 'build.gradle',
302
- 'Makefile', 'Dockerfile', 'docker-compose.yml',
303
- 'README.md', 'main.py', 'app.py', 'index.js', 'index.ts',
304
- ];
305
- for (const m of markers) {
306
- if (fs.existsSync(path.join(projectRoot, m))) return true;
307
- }
308
- for (const d of ['src', 'lib', 'app', 'backend', 'frontend', 'web', 'server', 'client']) {
309
- if (fs.existsSync(path.join(projectRoot, d)) && fs.statSync(path.join(projectRoot, d)).isDirectory()) return true;
310
- }
311
- return false;
312
- }
313
-
314
- module.exports = {
315
- loadSDK,
316
- runCodingSession,
317
- runScanSession,
318
- runAddSession,
319
- hasCodeFiles,
320
- };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { paths, loadConfig, buildEnvVars, getAllowedTools, log } = require('./config');
6
+ const { Indicator } = require('./indicator');
7
+ const { createSessionHooks } = require('./hooks');
8
+ const { buildSystemPrompt, buildCodingPrompt, buildScanPrompt, buildAddSystemPrompt, buildAddPrompt } = require('./prompts');
9
+
10
+ // ── SDK loader (cached, shared across sessions) ──
11
+
12
+ let _sdkModule = null;
13
+ async function loadSDK() {
14
+ if (_sdkModule) return _sdkModule;
15
+
16
+ const pkgName = '@anthropic-ai/claude-agent-sdk';
17
+ const attempts = [
18
+ () => import(pkgName),
19
+ () => {
20
+ const { createRequire } = require('module');
21
+ const resolved = createRequire(__filename).resolve(pkgName);
22
+ return import(resolved);
23
+ },
24
+ () => {
25
+ const { createRequire } = require('module');
26
+ const resolved = createRequire(path.join(process.cwd(), 'noop.js')).resolve(pkgName);
27
+ return import(resolved);
28
+ },
29
+ () => {
30
+ const { execSync } = require('child_process');
31
+ const globalRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
32
+ const sdkDir = path.join(globalRoot, pkgName);
33
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(sdkDir, 'package.json'), 'utf8'));
34
+ const entry = pkgJson.exports?.['.'] || pkgJson.main || 'index.js';
35
+ const entryFile = typeof entry === 'object' ? (entry.import || entry.default || entry.node) : entry;
36
+ return import(path.join(sdkDir, entryFile));
37
+ },
38
+ ];
39
+
40
+ for (const attempt of attempts) {
41
+ try {
42
+ _sdkModule = await attempt();
43
+ return _sdkModule;
44
+ } catch { /* try next */ }
45
+ }
46
+
47
+ log('error', `未找到 ${pkgName}`);
48
+ log('error', `请先安装:npm install -g ${pkgName}`);
49
+ process.exit(1);
50
+ }
51
+
52
+ // ── Helpers ──
53
+
54
+ function applyEnvConfig(config) {
55
+ Object.assign(process.env, buildEnvVars(config));
56
+ }
57
+
58
+ function buildQueryOptions(config, opts = {}) {
59
+ const base = {
60
+ allowedTools: getAllowedTools(config),
61
+ permissionMode: 'bypassPermissions',
62
+ allowDangerouslySkipPermissions: true,
63
+ cwd: opts.projectRoot || process.cwd(),
64
+ env: buildEnvVars(config),
65
+ settingSources: ['project'],
66
+ };
67
+ if (config.maxTurns > 0) base.maxTurns = config.maxTurns;
68
+ if (opts.model) base.model = opts.model;
69
+ else if (config.model) base.model = config.model;
70
+ return base;
71
+ }
72
+
73
+ function extractResult(messages) {
74
+ for (let i = messages.length - 1; i >= 0; i--) {
75
+ if (messages[i].type === 'result') return messages[i];
76
+ }
77
+ return null;
78
+ }
79
+
80
+ function writeSessionSeparator(logStream, sessionNum, label) {
81
+ const sep = '='.repeat(60);
82
+ logStream.write(`\n${sep}\n[Session ${sessionNum}] ${label} ${new Date().toISOString()}\n${sep}\n`);
83
+ }
84
+
85
+ let _lastPrintedStatusKey = '';
86
+
87
+ function logMessage(message, logStream, indicator) {
88
+ if (message.type === 'assistant' && message.message?.content) {
89
+ for (const block of message.message.content) {
90
+ if (block.type === 'text' && block.text) {
91
+ if (indicator) {
92
+ process.stderr.write('\r\x1b[K');
93
+ const contentKey = `${indicator.phase}|${indicator.step}|${indicator.toolTarget}`;
94
+ if (contentKey !== _lastPrintedStatusKey) {
95
+ _lastPrintedStatusKey = contentKey;
96
+ const statusLine = indicator.getStatusLine();
97
+ if (statusLine) process.stderr.write(statusLine + '\n');
98
+ }
99
+ }
100
+ process.stdout.write(block.text);
101
+ if (logStream) logStream.write(block.text);
102
+ }
103
+ if (block.type === 'tool_use' && logStream) {
104
+ logStream.write(`[TOOL_USE] ${block.name}: ${JSON.stringify(block.input).slice(0, 300)}\n`);
105
+ }
106
+ }
107
+ }
108
+
109
+ if (message.type === 'tool_result' && logStream) {
110
+ const isErr = message.is_error || false;
111
+ const content = typeof message.content === 'string'
112
+ ? message.content.slice(0, 500)
113
+ : JSON.stringify(message.content).slice(0, 500);
114
+ if (isErr) {
115
+ logStream.write(`[TOOL_ERROR] ${content}\n`);
116
+ }
117
+ }
118
+ }
119
+
120
+ // ── Session runners ──
121
+
122
+ async function runCodingSession(sessionNum, opts = {}) {
123
+ const sdk = await loadSDK();
124
+ const config = loadConfig();
125
+ applyEnvConfig(config);
126
+ const indicator = new Indicator();
127
+
128
+ const prompt = buildCodingPrompt(sessionNum, opts);
129
+ const systemPrompt = buildSystemPrompt(false);
130
+
131
+ const p = paths();
132
+ const taskId = opts.taskId || 'unknown';
133
+ const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
134
+ const logFile = path.join(p.logsDir, `${taskId}_session_${sessionNum}_${dateStr}.log`);
135
+ const logStream = fs.createWriteStream(logFile, { flags: 'a' });
136
+
137
+ writeSessionSeparator(logStream, sessionNum, `coding task=${taskId}`);
138
+
139
+ const stallTimeoutMs = config.stallTimeout * 1000;
140
+ const completionTimeoutMs = config.completionTimeout * 1000;
141
+ const abortController = new AbortController();
142
+ const { hooks, cleanup, isStalled } = createSessionHooks(indicator, logStream, {
143
+ enableStallDetection: true,
144
+ stallTimeoutMs,
145
+ abortController,
146
+ enableEditGuard: true,
147
+ editThreshold: config.editThreshold,
148
+ completionTimeoutMs,
149
+ });
150
+
151
+ const stallTimeoutMin = Math.floor(stallTimeoutMs / 60000);
152
+ indicator.start(sessionNum, stallTimeoutMin);
153
+
154
+ try {
155
+ const queryOpts = buildQueryOptions(config, opts);
156
+ queryOpts.systemPrompt = systemPrompt;
157
+ queryOpts.hooks = hooks;
158
+ queryOpts.abortController = abortController;
159
+
160
+ const session = sdk.query({ prompt, options: queryOpts });
161
+
162
+ const collected = [];
163
+ for await (const message of session) {
164
+ if (isStalled()) {
165
+ log('warn', '停顿超时,中断消息循环');
166
+ break;
167
+ }
168
+ collected.push(message);
169
+ logMessage(message, logStream, indicator);
170
+ }
171
+
172
+ cleanup();
173
+ logStream.end();
174
+ indicator.stop();
175
+
176
+ const result = extractResult(collected);
177
+ const subtype = result?.subtype || 'unknown';
178
+ if (subtype !== 'success' && subtype !== 'unknown') {
179
+ log('warn', `session 结束原因: ${subtype} (turns: ${result?.num_turns ?? '?'})`);
180
+ }
181
+ if (logStream.writable) {
182
+ logStream.write(`[${new Date().toISOString()}] SESSION_END subtype=${subtype} turns=${result?.num_turns ?? '?'} cost=${result?.total_cost_usd ?? '?'}\n`);
183
+ }
184
+ return {
185
+ exitCode: isStalled() ? 2 : 0,
186
+ cost: result?.total_cost_usd ?? null,
187
+ tokenUsage: result?.usage ?? null,
188
+ logFile,
189
+ stalled: isStalled(),
190
+ subtype,
191
+ };
192
+ } catch (err) {
193
+ cleanup();
194
+ logStream.end();
195
+ indicator.stop();
196
+ log('error', `Claude SDK 错误: ${err.message}`);
197
+ return {
198
+ exitCode: 1,
199
+ cost: null,
200
+ tokenUsage: null,
201
+ logFile,
202
+ error: err.message,
203
+ };
204
+ }
205
+ }
206
+
207
+ async function runScanSession(requirement, opts = {}) {
208
+ const sdk = await loadSDK();
209
+ const config = loadConfig();
210
+ applyEnvConfig(config);
211
+ const indicator = new Indicator();
212
+
213
+ const projectType = hasCodeFiles(opts.projectRoot || process.cwd()) ? 'existing' : 'new';
214
+ const prompt = buildScanPrompt(projectType, requirement);
215
+ const systemPrompt = buildSystemPrompt(true);
216
+
217
+ const p = paths();
218
+ const logFile = path.join(p.logsDir, `scan_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.log`);
219
+ const logStream = fs.createWriteStream(logFile, { flags: 'a' });
220
+
221
+ writeSessionSeparator(logStream, 0, `scan (${projectType})`);
222
+
223
+ const stallTimeoutMs = config.stallTimeout * 1000;
224
+ const completionTimeoutMs = config.completionTimeout * 1000;
225
+ const abortController = new AbortController();
226
+ const { hooks, cleanup, isStalled } = createSessionHooks(indicator, logStream, {
227
+ enableStallDetection: true,
228
+ stallTimeoutMs,
229
+ abortController,
230
+ completionTimeoutMs,
231
+ });
232
+
233
+ indicator.start(0, Math.floor(stallTimeoutMs / 60000));
234
+ log('info', `正在调用 Claude Code 执行项目扫描(${projectType}项目)...`);
235
+
236
+ try {
237
+ const queryOpts = buildQueryOptions(config, opts);
238
+ queryOpts.systemPrompt = systemPrompt;
239
+ queryOpts.hooks = hooks;
240
+ queryOpts.abortController = abortController;
241
+
242
+ const session = sdk.query({ prompt, options: queryOpts });
243
+
244
+ const collected = [];
245
+ for await (const message of session) {
246
+ if (isStalled()) {
247
+ log('warn', '扫描停顿超时,中断');
248
+ break;
249
+ }
250
+ collected.push(message);
251
+ logMessage(message, logStream, indicator);
252
+ }
253
+
254
+ cleanup();
255
+ logStream.end();
256
+ indicator.stop();
257
+
258
+ const result = extractResult(collected);
259
+ return {
260
+ exitCode: isStalled() ? 2 : 0,
261
+ cost: result?.total_cost_usd ?? null,
262
+ logFile,
263
+ stalled: isStalled(),
264
+ };
265
+ } catch (err) {
266
+ cleanup();
267
+ logStream.end();
268
+ indicator.stop();
269
+ log('error', `扫描失败: ${err.message}`);
270
+ return { exitCode: 1, cost: null, logFile, error: err.message };
271
+ }
272
+ }
273
+
274
+ async function runAddSession(instruction, opts = {}) {
275
+ const sdk = await loadSDK();
276
+ const config = loadConfig();
277
+ applyEnvConfig(config);
278
+ const indicator = new Indicator();
279
+
280
+ const systemPrompt = buildAddSystemPrompt();
281
+ const prompt = buildAddPrompt(instruction);
282
+
283
+ const p = paths();
284
+ const logFile = path.join(p.logsDir, `add_tasks_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.log`);
285
+ const logStream = fs.createWriteStream(logFile, { flags: 'a' });
286
+
287
+ writeSessionSeparator(logStream, 0, 'add tasks');
288
+
289
+ const stallTimeoutMs = config.stallTimeout * 1000;
290
+ const completionTimeoutMs = config.completionTimeout * 1000;
291
+ const abortController = new AbortController();
292
+ const { hooks, cleanup, isStalled } = createSessionHooks(indicator, logStream, {
293
+ enableStallDetection: true,
294
+ stallTimeoutMs,
295
+ abortController,
296
+ completionTimeoutMs,
297
+ });
298
+
299
+ indicator.start(0, Math.floor(stallTimeoutMs / 60000));
300
+ log('info', '正在追加任务...');
301
+
302
+ try {
303
+ const queryOpts = buildQueryOptions(config, opts);
304
+ queryOpts.systemPrompt = systemPrompt;
305
+ queryOpts.hooks = hooks;
306
+ queryOpts.abortController = abortController;
307
+
308
+ const session = sdk.query({ prompt, options: queryOpts });
309
+
310
+ for await (const message of session) {
311
+ if (isStalled()) {
312
+ log('warn', '追加任务停顿超时,中断');
313
+ break;
314
+ }
315
+ logMessage(message, logStream, indicator);
316
+ }
317
+
318
+ cleanup();
319
+ logStream.end();
320
+ indicator.stop();
321
+ log('ok', '任务追加完成');
322
+ } catch (err) {
323
+ cleanup();
324
+ logStream.end();
325
+ indicator.stop();
326
+ log('error', `任务追加失败: ${err.message}`);
327
+ }
328
+ }
329
+
330
+ function hasCodeFiles(projectRoot) {
331
+ const markers = [
332
+ 'package.json', 'pyproject.toml', 'requirements.txt', 'setup.py',
333
+ 'Cargo.toml', 'go.mod', 'pom.xml', 'build.gradle',
334
+ 'Makefile', 'Dockerfile', 'docker-compose.yml',
335
+ 'README.md', 'main.py', 'app.py', 'index.js', 'index.ts',
336
+ ];
337
+ for (const m of markers) {
338
+ if (fs.existsSync(path.join(projectRoot, m))) return true;
339
+ }
340
+ for (const d of ['src', 'lib', 'app', 'backend', 'frontend', 'web', 'server', 'client']) {
341
+ if (fs.existsSync(path.join(projectRoot, d)) && fs.statSync(path.join(projectRoot, d)).isDirectory()) return true;
342
+ }
343
+ return false;
344
+ }
345
+
346
+ module.exports = {
347
+ loadSDK,
348
+ runCodingSession,
349
+ runScanSession,
350
+ runAddSession,
351
+ hasCodeFiles,
352
+ };