-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathcli.ts
More file actions
192 lines (162 loc) · 7.11 KB
/
Copy pathcli.ts
File metadata and controls
192 lines (162 loc) · 7.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import { parseArgs } from 'util';
import { Agent } from '../src/core/agent';
import { loadInstructions } from '../src/core/prompt';
import { createModelFromEnv } from '../src/providers/modelFactory';
import { readFile, writeFile, editFile, execCommand } from '../src/tools';
import { createBranch, commit, pushBranch } from '../src/tools/git';
import { createPullRequest, createIssueComment } from '../src/tools/github';
import { mkdirSync, existsSync } from 'fs';
import { join } from 'path';
import { config } from '../src/config';
// 機密情報をマスクする(ログ出力用)
function maskSecret(value: string | undefined): string {
if (!value) return '(未設定)';
if (value.length <= 8) return '***';
return value.slice(0, 4) + '***' + value.slice(-4);
}
const WORKSPACE_ROOT = join(process.cwd(), 'workspace');
async function main() {
const { values, positionals } = parseArgs({
args: process.argv.slice(2),
options: {
'yolo': { type: 'boolean', default: false },
'stream': { type: 'boolean', default: false },
'responses': { type: 'boolean', default: false },
'sandbox': { type: 'boolean', default: false },
'allowed-domains': { type: 'string' },
},
allowPositionals: true,
});
const yoloMode = values['yolo'] ?? false;
const streamMode = values['stream'] ?? false;
const responsesMode = values['responses'] ?? false;
// configに反映
config.sandbox = values['sandbox'] ?? false;
if (values['allowed-domains']) {
config.allowedDomains.push(...values['allowed-domains'].split(','));
}
// --- 入力の取得 ---
// 1. CLI引数を優先
// 2. なければ環境変数 ISSUE_BODY(手動入力)を使用
// 3. なければ ISSUE_TEXT(Issue本文)があればIssue駆動モード
let userPrompt = positionals.join(' ');
const isIssueDriven = !userPrompt && !!(process.env.ISSUE_BODY || process.env.ISSUE_TEXT);
if (!userPrompt) {
userPrompt = process.env.ISSUE_BODY || process.env.ISSUE_TEXT || '';
}
if (!userPrompt) {
console.error('エラー: タスク内容を指定してください');
console.error('使用法: bun run bin/cli.ts "タスク内容" [--yolo]');
console.error('または環境変数 ISSUE_BODY を設定してください');
process.exit(1);
}
// --- 環境設定 ---
// ワークスペースディレクトリを作成
if (!existsSync(WORKSPACE_ROOT)) {
mkdirSync(WORKSPACE_ROOT, { recursive: true });
}
const provider = process.env.LLM_PROVIDER;
const modelName = process.env.LLM_MODEL;
const apiKey = process.env.LLM_API_KEY;
// GitHub Actions環境での実行かどうかを簡易判定(CI=trueなど)
const isCI = process.env.CI === 'true';
console.log('=== Nano Code Agent ===\n');
console.log(`Provider: ${provider || '(未設定)'}`);
console.log(`Model: ${modelName || '(未設定)'}`);
if (isCI) {
console.log(`API Key: ${maskSecret(apiKey)}`);
if (apiKey) {
console.log(`::add-mask::${apiKey}`);
}
}
console.log(`Workspace: ${WORKSPACE_ROOT}`);
if (isIssueDriven) {
console.log('[モード] Issue駆動モード (CI)');
}
if (yoloMode) {
console.log('[モード] 自動承認モード (--yolo)');
}
if (streamMode) {
console.log('[モード] ストリーミングモード (--stream)');
}
if (responsesMode) {
console.log('[モード] Responses API使用 (--responses)');
}
if (config.sandbox) {
console.log('[モード] サンドボックスモード (--sandbox)');
}
console.log(`Task: ${userPrompt.slice(0, 100)}${userPrompt.length > 100 ? '...' : ''}\n`);
if (!provider || !modelName || !apiKey) {
console.error('[ERROR] LLM設定が不足しています');
process.exit(1);
}
const model = createModelFromEnv({ useResponses: responsesMode });
// --- プロンプトの切り替え ---
// Issue駆動(CI実行)とそれ以外(ローカル実行)で指示を分ける
const baseInstructions = loadInstructions(WORKSPACE_ROOT);
const localInstructions = baseInstructions;
const issueText = process.env.ISSUE_TEXT || '';
const issueDrivenInstructions = `${baseInstructions}
あなたは GitHub Actions で実行される TypeScript コーディングエージェントです。
現在の環境は CI 環境であり、あなたの仕事はコードを修正してプルリクエストを作成することです。
トリガーとなった Issue 番号は ${process.env.ISSUE_NUMBER || '(なし)'} です(もし「(なし)」ならコメントは不要)。
## Issue本文(参照用)
${issueText}
## ワークフロー
以下の手順で作業を進めてください:
1. **TODOリストの作成**: Issueの内容に基づき、以下の項目を含むTODOリストを作成する。
- [ ] Issue を理解する
- [ ] 対象ファイルを読み込む
- [ ] コードを修正する
- [ ] 修正結果をテストする
- [ ] Git にコミットしてプッシュする
- [ ] プルリクエストを作成する
- [ ] 元の Issue にコメントで報告する
2. **タスクの実行**: TODOリストに従って作業を進める。
- **重要**: ファイルを修正しただけでは終了ではない。必ず Git コミット、プッシュ、プルリクエスト作成まで行うこと。
- 最後に createIssueComment を使い、作成したプルリクエストのURLを元のIssueに投稿すること。
3. **完了報告**: すべてのTODOが完了したら、結果をまとめる。
`;
const agent = new Agent({
name: 'nano-code',
model,
instructions: isIssueDriven ? issueDrivenInstructions : localInstructions,
tools: {
readFile,
writeFile,
editFile,
execCommand,
createBranch,
commit,
pushBranch,
createPullRequest,
createIssueComment,
},
maxSteps: 30,
useStreaming: streamMode, // 付録A(ストリーミング機能)用フラグ
// Yoloモードなら自動承認
approvalFunc: yoloMode ? async (name) => {
console.log(`[自動承認] ツール ${name} の実行を承認しました`);
return true;
} : undefined,
});
try {
const result = await agent.generate(userPrompt);
if (isCI) {
console.log('\n' + '─'.repeat(60));
console.log(`[完了] 正常終了`);
}
} catch (error) {
console.error('\n' + '─'.repeat(60));
console.error('[ERROR] エージェント実行中にエラーが発生しました\n');
if (error instanceof Error) {
let message = error.message;
if (apiKey) {
message = message.replace(new RegExp(apiKey, 'g'), maskSecret(apiKey));
}
console.error(`原因: ${message}`);
}
process.exit(1);
}
}
main();