claude-coder 1.0.1 → 1.0.3

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/bin/cli.js CHANGED
@@ -46,6 +46,9 @@ function parseArgs(argv) {
46
46
  case '--dry-run':
47
47
  opts.dryRun = true;
48
48
  break;
49
+ case '--view':
50
+ opts.viewMode = true;
51
+ break;
49
52
  case '--help':
50
53
  case '-h':
51
54
  showHelp();
@@ -78,7 +81,11 @@ async function main() {
78
81
  switch (command) {
79
82
  case 'run': {
80
83
  const runner = require('../src/runner');
81
- await runner.run(positional[0] || null, opts);
84
+ if (opts.viewMode) {
85
+ await runner.view(positional[0] || null, opts);
86
+ } else {
87
+ await runner.run(positional[0] || null, opts);
88
+ }
82
89
  break;
83
90
  }
84
91
  case 'setup': {
@@ -278,7 +278,65 @@ sequenceDiagram
278
278
 
279
279
  ---
280
280
 
281
- ## 9. 后续优化方向
281
+ ## 9. Claude Agent SDK V1/V2 对比与迁移计划
282
+
283
+ 当前使用 **V1 稳定 API**(`query()`),V2 为 preview 状态(`unstable_` 前缀)。
284
+
285
+ ### V1 vs V2 API 对比
286
+
287
+ | 维度 | V1 `query()` | V2 `send()/stream()` |
288
+ |------|-------------|---------------------|
289
+ | **状态** | 稳定,生产可用 | `unstable_` 前缀,preview |
290
+ | **入口函数** | `query({ prompt, options })` | `unstable_v2_createSession(opts)` / `unstable_v2_prompt()` |
291
+ | **多轮会话** | 需手动管理 AsyncGenerator | `session.send()` + `session.stream()`,更简洁 |
292
+ | **会话恢复** | `options.resume: sessionId` | `unstable_v2_resumeSession(id)` |
293
+ | **Hooks** | `options.hooks: { PreToolUse, PostToolUse, ... }` | 未支持 |
294
+ | **Subagents** | `options.agents: { name: AgentDefinition }` | 未支持 |
295
+ | **Session Fork** | `options.forkSession: true` | 未支持 |
296
+ | **Plugins** | `options.plugins: [{ type, path }]` | 未支持 |
297
+ | **结构化输出** | `options.outputFormat: { type: 'json_schema', schema }` | 支持 |
298
+ | **文件检查点** | `options.enableFileCheckpointing + rewindFiles()` | 未明确 |
299
+ | **Cost Tracking** | `SDKResultMessage.total_cost_usd` | `SDKResultMessage.total_cost_usd` |
300
+ | **权限控制** | `canUseTool`, `permissionMode`, `allowedTools`, `disallowedTools` | 继承 |
301
+
302
+ ### 当前实现使用的 V1 特性
303
+
304
+ ```javascript
305
+ query({
306
+ prompt,
307
+ options: {
308
+ systemPrompt, // 注入 CLAUDE.md
309
+ allowedTools, // 工具白名单
310
+ permissionMode: 'bypassPermissions',
311
+ allowDangerouslySkipPermissions: true,
312
+ model, // 从 .env 传入
313
+ env, // 环境变量透传
314
+ settingSources: ['project'], // 加载项目 CLAUDE.md
315
+ hooks: { PreToolUse: [...] }, // 实时 spinner 监控
316
+ }
317
+ })
318
+ ```
319
+
320
+ ### V2 迁移条件(等待稳定后)
321
+
322
+ 1. V2 去掉 `unstable_` 前缀,正式发布
323
+ 2. V2 支持 Hooks(当前项目依赖 PreToolUse 做 spinner 和 activity log)
324
+ 3. V2 支持 Subagents(未来可能用于扫描 Agent / 编码 Agent 分离)
325
+
326
+ ### 可利用但尚未使用的 V1 特性
327
+
328
+ | 特性 | 说明 | 优先级 |
329
+ |------|------|--------|
330
+ | `maxBudgetUsd` | SDK 内置成本上限,替代自研追踪 | P0 |
331
+ | `effort` | 控制思考深度(`low`/`medium`/`high`/`max`) | P1 |
332
+ | `enableFileCheckpointing` | 文件操作检查点,比 git reset 更精细 | P1 |
333
+ | `outputFormat` | 结构化输出,让 Agent 直接输出 JSON 格式 | P1 |
334
+ | `agents` | 定义子 Agent,不同模型/工具集 | P2 |
335
+ | `betas` | 扩展上下文窗口 | P2 |
336
+
337
+ ---
338
+
339
+ ## 10. 后续优化方向
282
340
 
283
341
  ### P0 — 近期
284
342
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-coder",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Claude Coder — Autonomous coding agent harness powered by Claude Code SDK. Scan, plan, code, validate, git-commit in a loop.",
5
5
  "bin": {
6
6
  "claude-coder": "bin/cli.js"
package/src/runner.js CHANGED
@@ -12,14 +12,28 @@ const { runCodingSession, runViewSession, runAddSession } = require('./session')
12
12
 
13
13
  const MAX_RETRY = 3;
14
14
 
15
- function requireSdk() {
16
- try {
17
- require.resolve('@anthropic-ai/claude-agent-sdk');
18
- } catch {
19
- console.error('错误:未找到 @anthropic-ai/claude-agent-sdk');
20
- console.error('请先安装:npm install -g @anthropic-ai/claude-agent-sdk');
21
- process.exit(1);
15
+ async function requireSdk() {
16
+ const pkgName = '@anthropic-ai/claude-agent-sdk';
17
+ const attempts = [
18
+ () => { require.resolve(pkgName); return true; },
19
+ () => {
20
+ const { createRequire } = require('module');
21
+ createRequire(__filename).resolve(pkgName);
22
+ return true;
23
+ },
24
+ () => {
25
+ const prefix = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
26
+ const sdkPath = path.join(prefix, 'lib', 'node_modules', pkgName);
27
+ if (fs.existsSync(sdkPath)) return true;
28
+ throw new Error('not found');
29
+ },
30
+ ];
31
+ for (const attempt of attempts) {
32
+ try { if (attempt()) return; } catch { /* try next */ }
22
33
  }
34
+ console.error(`错误:未找到 ${pkgName}`);
35
+ console.error(`请先安装:npm install -g ${pkgName}`);
36
+ process.exit(1);
23
37
  }
24
38
 
25
39
  function getHead() {
@@ -86,7 +100,7 @@ function appendProgress(entry) {
86
100
  const p = paths();
87
101
  let progress = { sessions: [] };
88
102
  if (fs.existsSync(p.progressFile)) {
89
- try { progress = JSON.parse(fs.readFileSync(p.progressFile, 'utf8')); } catch { /* reset */ }
103
+ try { progress = JSON.parse(fs.readFileSync(p.progressFile, 'utf8').replace(/[\u201c\u201d]/g, '"')); } catch { /* reset */ }
90
104
  }
91
105
  if (!Array.isArray(progress.sessions)) progress.sessions = [];
92
106
  progress.sessions.push(entry);
@@ -97,7 +111,7 @@ function updateSessionHistory(sessionData, sessionNum) {
97
111
  const p = paths();
98
112
  let sr = { current: null, history: [] };
99
113
  if (fs.existsSync(p.sessionResult)) {
100
- try { sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8')); } catch { /* reset */ }
114
+ try { sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8').replace(/[\u201c\u201d]/g, '"')); } catch { /* reset */ }
101
115
  if (!sr.history && sr.session_result) {
102
116
  sr = { current: sr, history: [] };
103
117
  }
@@ -199,7 +213,7 @@ async function run(requirement, opts = {}) {
199
213
  return;
200
214
  }
201
215
 
202
- requireSdk();
216
+ await requireSdk();
203
217
  const scanResult = await scan(requirement, { projectRoot });
204
218
  if (!scanResult.success) {
205
219
  console.log('');
@@ -215,7 +229,7 @@ async function run(requirement, opts = {}) {
215
229
  }
216
230
 
217
231
  // Coding loop
218
- if (!dryRun) requireSdk();
232
+ if (!dryRun) await requireSdk();
219
233
  log('info', `开始编码循环 (最多 ${maxSessions} 个会话) ...`);
220
234
  console.log('');
221
235
 
@@ -319,7 +333,7 @@ async function run(requirement, opts = {}) {
319
333
  }
320
334
 
321
335
  async function view(requirement, opts = {}) {
322
- requireSdk();
336
+ await requireSdk();
323
337
  const projectRoot = getProjectRoot();
324
338
  ensureLoopDir();
325
339
 
@@ -331,7 +345,7 @@ async function view(requirement, opts = {}) {
331
345
  }
332
346
 
333
347
  async function add(instruction, opts = {}) {
334
- requireSdk();
348
+ await requireSdk();
335
349
  const p = paths();
336
350
  const projectRoot = getProjectRoot();
337
351
  ensureLoopDir();
package/src/session.js CHANGED
@@ -6,12 +6,76 @@ const { paths, loadConfig, buildEnvVars, getAllowedTools, log } = require('./con
6
6
  const { Indicator, inferPhaseStep } = require('./indicator');
7
7
  const { buildSystemPrompt, buildCodingPrompt, buildScanPrompt, buildViewPrompt, buildAddPrompt } = require('./prompts');
8
8
 
9
+ let _sdkModule = null;
10
+ async function loadSDK() {
11
+ if (_sdkModule) return _sdkModule;
12
+
13
+ const pkgName = '@anthropic-ai/claude-agent-sdk';
14
+ const attempts = [
15
+ () => import(pkgName),
16
+ () => {
17
+ const { createRequire } = require('module');
18
+ const resolved = createRequire(__filename).resolve(pkgName);
19
+ return import(resolved);
20
+ },
21
+ () => {
22
+ const { execSync } = require('child_process');
23
+ const prefix = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
24
+ const sdkPath = path.join(prefix, 'lib', 'node_modules', pkgName, 'sdk.mjs');
25
+ return import(sdkPath);
26
+ },
27
+ ];
28
+
29
+ for (const attempt of attempts) {
30
+ try {
31
+ _sdkModule = await attempt();
32
+ return _sdkModule;
33
+ } catch { /* try next */ }
34
+ }
35
+
36
+ log('error', `未找到 ${pkgName}`);
37
+ log('error', `请先安装:npm install -g ${pkgName}`);
38
+ process.exit(1);
39
+ }
40
+
9
41
  function applyEnvConfig(config) {
10
42
  Object.assign(process.env, buildEnvVars(config));
11
43
  }
12
44
 
45
+ function buildQueryOptions(config, opts = {}) {
46
+ const base = {
47
+ allowedTools: getAllowedTools(config),
48
+ permissionMode: 'bypassPermissions',
49
+ allowDangerouslySkipPermissions: true,
50
+ cwd: opts.projectRoot || process.cwd(),
51
+ env: buildEnvVars(config),
52
+ settingSources: ['project'],
53
+ };
54
+ if (config.model) base.model = config.model;
55
+ return base;
56
+ }
57
+
58
+ function extractResult(messages) {
59
+ for (let i = messages.length - 1; i >= 0; i--) {
60
+ if (messages[i].type === 'result') return messages[i];
61
+ }
62
+ return null;
63
+ }
64
+
65
+ function logMessage(message, logStream, indicator) {
66
+ if (message.type === 'assistant' && message.message?.content) {
67
+ for (const block of message.message.content) {
68
+ if (block.type === 'text' && block.text) {
69
+ if (indicator) process.stderr.write('\r\x1b[K');
70
+ process.stdout.write(block.text);
71
+ if (logStream) logStream.write(block.text);
72
+ }
73
+ }
74
+ }
75
+ }
76
+
13
77
  async function runCodingSession(sessionNum, opts = {}) {
14
- const { query } = require('@anthropic-ai/claude-agent-sdk');
78
+ const sdk = await loadSDK();
15
79
  const config = loadConfig();
16
80
  applyEnvConfig(config);
17
81
  const indicator = new Indicator();
@@ -26,46 +90,34 @@ async function runCodingSession(sessionNum, opts = {}) {
26
90
  indicator.start(sessionNum);
27
91
 
28
92
  try {
29
- const session = query({
30
- prompt,
31
- options: {
32
- systemPrompt,
33
- allowedTools: getAllowedTools(config),
34
- permissionMode: 'bypassPermissions',
35
- verbose: true,
36
- cwd: opts.projectRoot || process.cwd(),
37
- timeout_ms: config.timeoutMs,
38
- hooks: {
39
- PreToolUse: [{
40
- matcher: '*',
41
- callback: (event) => {
42
- inferPhaseStep(indicator, event.tool_name, event.tool_input);
43
- return { decision: 'allow' };
44
- }
45
- }]
46
- }
47
- }
48
- });
93
+ const queryOpts = buildQueryOptions(config, opts);
94
+ queryOpts.systemPrompt = systemPrompt;
95
+ queryOpts.hooks = {
96
+ PreToolUse: [{
97
+ matcher: '*',
98
+ hooks: [async (input) => {
99
+ inferPhaseStep(indicator, input.tool_name, input.tool_input);
100
+ return {};
101
+ }]
102
+ }]
103
+ };
104
+
105
+ const session = sdk.query({ prompt, options: queryOpts });
49
106
 
50
- let result;
107
+ const collected = [];
51
108
  for await (const message of session) {
52
- if (message.content) {
53
- const text = typeof message.content === 'string'
54
- ? message.content
55
- : JSON.stringify(message.content);
56
- process.stdout.write(text);
57
- logStream.write(text);
58
- }
59
- result = message;
109
+ collected.push(message);
110
+ logMessage(message, logStream, indicator);
60
111
  }
61
112
 
62
113
  logStream.end();
63
114
  indicator.stop();
64
115
 
116
+ const result = extractResult(collected);
65
117
  return {
66
118
  exitCode: 0,
67
- cost: result?.total_cost_usd || null,
68
- tokenUsage: result?.message?.usage || null,
119
+ cost: result?.total_cost_usd ?? null,
120
+ tokenUsage: result?.usage ?? null,
69
121
  logFile,
70
122
  };
71
123
  } catch (err) {
@@ -83,7 +135,7 @@ async function runCodingSession(sessionNum, opts = {}) {
83
135
  }
84
136
 
85
137
  async function runScanSession(requirement, opts = {}) {
86
- const { query } = require('@anthropic-ai/claude-agent-sdk');
138
+ const sdk = await loadSDK();
87
139
  const config = loadConfig();
88
140
  applyEnvConfig(config);
89
141
  const indicator = new Indicator();
@@ -100,45 +152,33 @@ async function runScanSession(requirement, opts = {}) {
100
152
  log('info', `正在调用 Claude Code 执行项目扫描(${projectType}项目)...`);
101
153
 
102
154
  try {
103
- const session = query({
104
- prompt,
105
- options: {
106
- systemPrompt,
107
- allowedTools: getAllowedTools(config),
108
- permissionMode: 'bypassPermissions',
109
- verbose: true,
110
- cwd: opts.projectRoot || process.cwd(),
111
- timeout_ms: config.timeoutMs,
112
- hooks: {
113
- PreToolUse: [{
114
- matcher: '*',
115
- callback: (event) => {
116
- inferPhaseStep(indicator, event.tool_name, event.tool_input);
117
- return { decision: 'allow' };
118
- }
119
- }]
120
- }
121
- }
122
- });
155
+ const queryOpts = buildQueryOptions(config, opts);
156
+ queryOpts.systemPrompt = systemPrompt;
157
+ queryOpts.hooks = {
158
+ PreToolUse: [{
159
+ matcher: '*',
160
+ hooks: [async (input) => {
161
+ inferPhaseStep(indicator, input.tool_name, input.tool_input);
162
+ return {};
163
+ }]
164
+ }]
165
+ };
166
+
167
+ const session = sdk.query({ prompt, options: queryOpts });
123
168
 
124
- let result;
169
+ const collected = [];
125
170
  for await (const message of session) {
126
- if (message.content) {
127
- const text = typeof message.content === 'string'
128
- ? message.content
129
- : JSON.stringify(message.content);
130
- process.stdout.write(text);
131
- logStream.write(text);
132
- }
133
- result = message;
171
+ collected.push(message);
172
+ logMessage(message, logStream, indicator);
134
173
  }
135
174
 
136
175
  logStream.end();
137
176
  indicator.stop();
138
177
 
178
+ const result = extractResult(collected);
139
179
  return {
140
180
  exitCode: 0,
141
- cost: result?.total_cost_usd || null,
181
+ cost: result?.total_cost_usd ?? null,
142
182
  logFile,
143
183
  };
144
184
  } catch (err) {
@@ -150,7 +190,7 @@ async function runScanSession(requirement, opts = {}) {
150
190
  }
151
191
 
152
192
  async function runViewSession(requirement, opts = {}) {
153
- const { query } = require('@anthropic-ai/claude-agent-sdk');
193
+ const sdk = await loadSDK();
154
194
  const p = paths();
155
195
  const config = loadConfig();
156
196
  applyEnvConfig(config);
@@ -172,25 +212,13 @@ async function runViewSession(requirement, opts = {}) {
172
212
  }
173
213
 
174
214
  try {
175
- const session = query({
176
- prompt,
177
- options: {
178
- systemPrompt,
179
- allowedTools: getAllowedTools(config),
180
- permissionMode: 'bypassPermissions',
181
- verbose: true,
182
- cwd: opts.projectRoot || process.cwd(),
183
- timeout_ms: config.timeoutMs,
184
- }
185
- });
215
+ const queryOpts = buildQueryOptions(config, opts);
216
+ queryOpts.systemPrompt = systemPrompt;
217
+
218
+ const session = sdk.query({ prompt, options: queryOpts });
186
219
 
187
220
  for await (const message of session) {
188
- if (message.content) {
189
- const text = typeof message.content === 'string'
190
- ? message.content
191
- : JSON.stringify(message.content);
192
- process.stdout.write(text);
193
- }
221
+ logMessage(message, null);
194
222
  }
195
223
  } catch (err) {
196
224
  log('error', `观测模式错误: ${err.message}`);
@@ -198,7 +226,7 @@ async function runViewSession(requirement, opts = {}) {
198
226
  }
199
227
 
200
228
  async function runAddSession(instruction, opts = {}) {
201
- const { query } = require('@anthropic-ai/claude-agent-sdk');
229
+ const sdk = await loadSDK();
202
230
  const config = loadConfig();
203
231
  applyEnvConfig(config);
204
232
 
@@ -210,26 +238,13 @@ async function runAddSession(instruction, opts = {}) {
210
238
  const logStream = fs.createWriteStream(logFile, { flags: 'a' });
211
239
 
212
240
  try {
213
- const session = query({
214
- prompt,
215
- options: {
216
- systemPrompt,
217
- allowedTools: getAllowedTools(config),
218
- permissionMode: 'bypassPermissions',
219
- verbose: true,
220
- cwd: opts.projectRoot || process.cwd(),
221
- timeout_ms: config.timeoutMs,
222
- }
223
- });
241
+ const queryOpts = buildQueryOptions(config, opts);
242
+ queryOpts.systemPrompt = systemPrompt;
243
+
244
+ const session = sdk.query({ prompt, options: queryOpts });
224
245
 
225
246
  for await (const message of session) {
226
- if (message.content) {
227
- const text = typeof message.content === 'string'
228
- ? message.content
229
- : JSON.stringify(message.content);
230
- process.stdout.write(text);
231
- logStream.write(text);
232
- }
247
+ logMessage(message, logStream);
233
248
  }
234
249
 
235
250
  logStream.end();
package/src/tasks.js CHANGED
@@ -13,10 +13,16 @@ const TRANSITIONS = {
13
13
  done: [],
14
14
  };
15
15
 
16
+ function normalizeJson(text) {
17
+ return text
18
+ .replace(/[\u201c\u201d]/g, '"')
19
+ .replace(/[\u2018\u2019]/g, "'");
20
+ }
21
+
16
22
  function loadTasks() {
17
23
  const p = paths();
18
24
  if (!fs.existsSync(p.tasksFile)) return null;
19
- return JSON.parse(fs.readFileSync(p.tasksFile, 'utf8'));
25
+ return JSON.parse(normalizeJson(fs.readFileSync(p.tasksFile, 'utf8')));
20
26
  }
21
27
 
22
28
  function saveTasks(data) {
package/src/validator.js CHANGED
@@ -4,6 +4,10 @@ const fs = require('fs');
4
4
  const { execSync } = require('child_process');
5
5
  const { paths, log, getProjectRoot } = require('./config');
6
6
 
7
+ function normalizeJson(text) {
8
+ return text.replace(/[\u201c\u201d]/g, '"').replace(/[\u2018\u2019]/g, "'");
9
+ }
10
+
7
11
  function validateSessionResult() {
8
12
  const p = paths();
9
13
 
@@ -14,7 +18,7 @@ function validateSessionResult() {
14
18
 
15
19
  let data;
16
20
  try {
17
- data = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
21
+ data = JSON.parse(normalizeJson(fs.readFileSync(p.sessionResult, 'utf8')));
18
22
  } catch {
19
23
  log('error', 'session_result.json JSON 格式错误');
20
24
  return { valid: false, fatal: true, reason: 'JSON 格式错误' };
@@ -86,9 +90,9 @@ function checkTestCoverage() {
86
90
  if (!fs.existsSync(p.testsFile) || !fs.existsSync(p.sessionResult)) return;
87
91
 
88
92
  try {
89
- const sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
93
+ const sr = JSON.parse(normalizeJson(fs.readFileSync(p.sessionResult, 'utf8')));
90
94
  const current = sr.current || sr;
91
- const tests = JSON.parse(fs.readFileSync(p.testsFile, 'utf8'));
95
+ const tests = JSON.parse(normalizeJson(fs.readFileSync(p.testsFile, 'utf8')));
92
96
 
93
97
  const taskId = current.task_id || '';
94
98
  const testCases = tests.test_cases || [];