@tinygem/soda-agent 0.1.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/dist/auth.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function isAuthRequired(): boolean;
2
+ export declare function isAuthenticated(): boolean;
3
+ export declare function authenticate(code: string): boolean;
package/dist/auth.js ADDED
@@ -0,0 +1,40 @@
1
+ // 초대 코드 기반 접근 제어
2
+ // SHA-256 해시로 코드 검증, ~/.soda/auth.json에 인증 캐시
3
+ import { createHash } from "node:crypto";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { homedir } from "node:os";
7
+ const AUTH_DIR = join(homedir(), ".soda");
8
+ const AUTH_FILE = join(AUTH_DIR, "auth.json");
9
+ // 유효한 초대 코드의 SHA-256 해시 목록
10
+ // 새 코드 추가: echo -n "코드" | sha256sum → 해시를 배열에 추가 → npm publish
11
+ const VALID_CODE_HASHES = [
12
+ "63480d1058157eac3adce6c45ba7da6ed320d4737c073296eda6705de8a4de5d", // cj26
13
+ ];
14
+ function hashCode(code) {
15
+ return createHash("sha256").update(code.trim()).digest("hex");
16
+ }
17
+ // 코드 해시 목록이 비어있으면 인증 불필요 (개발 모드)
18
+ export function isAuthRequired() {
19
+ return VALID_CODE_HASHES.length > 0;
20
+ }
21
+ export function isAuthenticated() {
22
+ if (!existsSync(AUTH_FILE))
23
+ return false;
24
+ try {
25
+ const data = JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
26
+ return typeof data.codeHash === "string" && VALID_CODE_HASHES.includes(data.codeHash);
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ export function authenticate(code) {
33
+ const hash = hashCode(code);
34
+ if (!VALID_CODE_HASHES.includes(hash))
35
+ return false;
36
+ mkdirSync(AUTH_DIR, { recursive: true });
37
+ writeFileSync(AUTH_FILE, JSON.stringify({ codeHash: hash, authenticatedAt: Date.now() }, null, 2), { mode: 0o600 });
38
+ return true;
39
+ }
40
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,iBAAiB;AACjB,8CAA8C;AAE9C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;AAC1C,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;AAE9C,2BAA2B;AAC3B,+DAA+D;AAC/D,MAAM,iBAAiB,GAAa;IAClC,kEAAkE,EAAE,OAAO;CAC5E,CAAC;AAEF,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC;AAED,iCAAiC;AACjC,MAAM,UAAU,cAAc;IAC5B,OAAO,iBAAiB,CAAC,MAAM,GAAG,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,KAAK,CAAC;IACzC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;QAC1D,OAAO,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ,IAAI,iBAAiB,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACpD,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,aAAa,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACpH,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Limits, RunState } from "./types.js";
2
+ export declare class CircuitBreakerError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export declare function checkCircuitBreaker(state: RunState, limits: Limits): void;
@@ -0,0 +1,23 @@
1
+ // 안전 장치: 반복/시간/연속 실패 제한
2
+ export class CircuitBreakerError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "CircuitBreakerError";
6
+ }
7
+ }
8
+ export function checkCircuitBreaker(state, limits) {
9
+ if (state.iterations >= limits.maxIterations) {
10
+ throw new CircuitBreakerError(`최대 반복 횟수(${limits.maxIterations})를 초과했습니다. ` +
11
+ `현재: ${state.iterations}회`);
12
+ }
13
+ const elapsedMinutes = (Date.now() - state.startTime) / 60_000;
14
+ if (elapsedMinutes >= limits.maxTimeMinutes) {
15
+ throw new CircuitBreakerError(`최대 실행 시간(${limits.maxTimeMinutes}분)을 초과했습니다. ` +
16
+ `경과: ${Math.round(elapsedMinutes)}분`);
17
+ }
18
+ if (state.consecutiveFailures >= limits.maxConsecutiveFails) {
19
+ throw new CircuitBreakerError(`연속 실패 횟수(${limits.maxConsecutiveFails})를 초과했습니다. ` +
20
+ `현재: ${state.consecutiveFailures}회 연속 실패`);
21
+ }
22
+ }
23
+ //# sourceMappingURL=circuit-breaker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuit-breaker.js","sourceRoot":"","sources":["../src/circuit-breaker.ts"],"names":[],"mappings":"AAAA,wBAAwB;AAIxB,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAe,EAAE,MAAc;IACjE,IAAI,KAAK,CAAC,UAAU,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;QAC7C,MAAM,IAAI,mBAAmB,CAC3B,YAAY,MAAM,CAAC,aAAa,aAAa;YAC3C,OAAO,KAAK,CAAC,UAAU,GAAG,CAC7B,CAAC;IACJ,CAAC;IAED,MAAM,cAAc,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC;IAC/D,IAAI,cAAc,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;QAC5C,MAAM,IAAI,mBAAmB,CAC3B,YAAY,MAAM,CAAC,cAAc,cAAc;YAC7C,OAAO,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,GAAG,CACvC,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,mBAAmB,IAAI,MAAM,CAAC,mBAAmB,EAAE,CAAC;QAC5D,MAAM,IAAI,mBAAmB,CAC3B,YAAY,MAAM,CAAC,mBAAmB,aAAa;YACjD,OAAO,KAAK,CAAC,mBAAmB,SAAS,CAC5C,CAAC;IACJ,CAAC;AACH,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env node
2
+ // soda CLI 진입점: 인자 파싱, 인증, 오케스트레이터 시작
3
+ import { readFileSync } from "node:fs";
4
+ import { createInterface } from "node:readline";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { DEFAULT_LIMITS, getErrorMessage } from "./types.js";
8
+ import { checkClaudeCLI } from "./executor.js";
9
+ import { run } from "./orchestrator.js";
10
+ import { logError, logInfo, logSuccess, logWarn } from "./logger.js";
11
+ import { clearState, cleanupPidFile, isLocked, writeClarifyPending, writePidFile } from "./state.js";
12
+ import { isAuthRequired, isAuthenticated, authenticate } from "./auth.js";
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ let VERSION = "0.1.0";
15
+ try {
16
+ VERSION = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version;
17
+ }
18
+ catch { /* dist 외부에서 실행 시 폴백 사용 */ }
19
+ const HELP = `
20
+ soda v${VERSION} — 자율 개발 에이전트
21
+
22
+ 사용법:
23
+ npx soda "작업 내용" 작업 실행
24
+ npx soda --resume 이전 작업 재개
25
+ npx soda --clear 저장된 작업 상태 삭제
26
+ npx soda --help 도움말
27
+
28
+ 옵션:
29
+ --max-iterations N 최대 반복 횟수 (기본: ${DEFAULT_LIMITS.maxIterations})
30
+ --max-time N 최대 실행 시간(분) (기본: ${DEFAULT_LIMITS.maxTimeMinutes})
31
+ --resume 이전 작업 재개
32
+ --help, -h 도움말
33
+
34
+ 사전 요구사항:
35
+ - Claude Code가 설치되어 있어야 합니다.
36
+ - claude --dangerously-skip-permissions 를 1회 실행하여 약관에 동의해야 합니다.
37
+ - root 사용자로는 실행할 수 없습니다.
38
+ `;
39
+ function parseArgs(argv) {
40
+ const result = {
41
+ goal: "",
42
+ resume: false,
43
+ limits: { ...DEFAULT_LIMITS },
44
+ clear: false,
45
+ help: false,
46
+ pluginMode: false,
47
+ };
48
+ const args = argv.slice(2); // node, script 제외
49
+ let i = 0;
50
+ while (i < args.length) {
51
+ const arg = args[i];
52
+ switch (arg) {
53
+ case "--help":
54
+ case "-h":
55
+ result.help = true;
56
+ break;
57
+ case "--resume":
58
+ result.resume = true;
59
+ break;
60
+ case "--clear":
61
+ result.clear = true;
62
+ break;
63
+ case "--max-iterations": {
64
+ i++;
65
+ const maxIter = parseInt(args[i] ?? "", 10);
66
+ if (Number.isFinite(maxIter) && maxIter > 0) {
67
+ result.limits.maxIterations = maxIter;
68
+ }
69
+ else {
70
+ logWarn(`잘못된 --max-iterations 값: "${args[i]}". 기본값 ${DEFAULT_LIMITS.maxIterations}을 사용합니다.`);
71
+ }
72
+ break;
73
+ }
74
+ case "--max-time": {
75
+ i++;
76
+ const maxTime = parseInt(args[i] ?? "", 10);
77
+ if (Number.isFinite(maxTime) && maxTime > 0) {
78
+ result.limits.maxTimeMinutes = maxTime;
79
+ }
80
+ else {
81
+ logWarn(`잘못된 --max-time 값: "${args[i]}". 기본값 ${DEFAULT_LIMITS.maxTimeMinutes}을 사용합니다.`);
82
+ }
83
+ break;
84
+ }
85
+ // 내부 플래그 — Claude Code 플러그인에서 자동으로 사용되며
86
+ // 사용자가 직접 사용할 필요 없으므로 --help에 표시하지 않음
87
+ case "--plugin-mode":
88
+ result.pluginMode = true;
89
+ break;
90
+ default:
91
+ if (arg.startsWith("--")) {
92
+ logWarn(`알 수 없는 옵션: ${arg}`);
93
+ }
94
+ else {
95
+ result.goal = result.goal ? `${result.goal} ${arg}` : arg;
96
+ }
97
+ break;
98
+ }
99
+ i++;
100
+ }
101
+ return result;
102
+ }
103
+ // CLARIFY 멀티턴 대화: NPX 모드 (터미널 입력)
104
+ function createTerminalInput() {
105
+ return async (clarifyOutput) => {
106
+ console.log(`\n${clarifyOutput}\n`);
107
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
108
+ try {
109
+ const answer = await new Promise((resolve) => {
110
+ rl.question("[soda] 답변을 입력하세요: ", resolve);
111
+ });
112
+ return answer.trim() || null;
113
+ }
114
+ finally {
115
+ rl.close();
116
+ }
117
+ };
118
+ }
119
+ // CLARIFY 멀티턴 대화: Plugin 모드 (파일 기반 시그널링)
120
+ function createPluginInput() {
121
+ return async (clarifyOutput, workingDir) => {
122
+ writeClarifyPending(workingDir, clarifyOutput);
123
+ logInfo("CLARIFY 질문이 .soda/clarify-pending.json에 저장되었습니다.");
124
+ return null;
125
+ };
126
+ }
127
+ async function main() {
128
+ const parsed = parseArgs(process.argv);
129
+ // 도움말
130
+ if (parsed.help) {
131
+ console.log(HELP);
132
+ process.exit(0);
133
+ }
134
+ // 상태 삭제
135
+ if (parsed.clear) {
136
+ clearState(process.cwd());
137
+ logInfo("작업 상태가 삭제되었습니다.");
138
+ process.exit(0);
139
+ }
140
+ // root 사용자 차단 (Claude Code가 root에서 --dangerously-skip-permissions 차단)
141
+ if (typeof process.getuid === "function" && process.getuid() === 0) {
142
+ logError("root 사용자로는 soda를 실행할 수 없습니다.");
143
+ logError("Claude Code가 root에서 --dangerously-skip-permissions를 차단합니다.");
144
+ logError("일반 사용자로 전환 후 실행하세요.");
145
+ process.exit(1);
146
+ }
147
+ // Claude CLI 확인
148
+ const hasClaude = await checkClaudeCLI();
149
+ if (!hasClaude) {
150
+ logError("claude CLI를 찾을 수 없습니다.");
151
+ logError("Claude Code를 먼저 설치하세요: https://docs.anthropic.com/en/docs/claude-code");
152
+ process.exit(1);
153
+ }
154
+ // 초대 코드 인증 확인
155
+ if (isAuthRequired() && !isAuthenticated()) {
156
+ const code = process.env.SODA_INVITE_CODE;
157
+ if (code) {
158
+ if (!authenticate(code)) {
159
+ logError("유효하지 않은 초대 코드입니다.");
160
+ process.exit(1);
161
+ }
162
+ logInfo("인증 완료.");
163
+ }
164
+ else {
165
+ // 터미널 입력으로 초대 코드 요청
166
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
167
+ const inputCode = await new Promise((resolve) => {
168
+ rl.question("[soda] 초대 코드를 입력하세요: ", resolve);
169
+ });
170
+ rl.close();
171
+ if (!authenticate(inputCode)) {
172
+ logError("유효하지 않은 초대 코드입니다.");
173
+ process.exit(1);
174
+ }
175
+ logSuccess("인증 완료!");
176
+ }
177
+ }
178
+ // goal 또는 resume 확인
179
+ if (!parsed.goal && !parsed.resume) {
180
+ logError("작업 내용을 입력하세요.");
181
+ console.log(HELP);
182
+ process.exit(1);
183
+ }
184
+ // goal과 resume 동시 지정 시 경고
185
+ if (parsed.goal && parsed.resume) {
186
+ logWarn("--resume과 작업 내용이 동시에 지정되었습니다. 이전 작업을 재개합니다.");
187
+ // resume 우선 — 새 goal은 무시
188
+ }
189
+ const cwd = process.cwd();
190
+ // 동시 실행 보호 — PID 기록 전에 확인
191
+ if (isLocked(cwd)) {
192
+ logError("다른 soda 인스턴스가 이 디렉토리에서 이미 실행 중입니다.");
193
+ process.exit(1);
194
+ }
195
+ // PID 파일 기록
196
+ writePidFile(cwd);
197
+ // 시작 메시지
198
+ logSuccess("soda 시작");
199
+ if (parsed.resume) {
200
+ logInfo("이전 작업을 재개합니다...");
201
+ }
202
+ else {
203
+ logInfo(`작업: ${parsed.goal}`);
204
+ }
205
+ // 오케스트레이터 실행
206
+ try {
207
+ await run({
208
+ goal: parsed.goal,
209
+ workingDir: cwd,
210
+ resume: parsed.resume,
211
+ limits: parsed.limits,
212
+ pluginMode: parsed.pluginMode,
213
+ getUserInput: parsed.pluginMode ? createPluginInput() : createTerminalInput(),
214
+ });
215
+ }
216
+ finally {
217
+ cleanupPidFile(cwd);
218
+ }
219
+ }
220
+ main().catch((err) => {
221
+ logError(`치명적 오류: ${getErrorMessage(err)}`);
222
+ process.exit(1);
223
+ });
224
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { PhaseConfig, PhaseResult } from "./types.js";
2
+ export declare function executeClaudePhase(config: PhaseConfig): Promise<PhaseResult>;
3
+ export declare function checkClaudeCLI(): Promise<boolean>;
@@ -0,0 +1,146 @@
1
+ // claude -p subprocess 실행기
2
+ // 각 호출은 새 세션 (fresh context) — 컨텍스트 열화 방지
3
+ import { spawn } from "node:child_process";
4
+ import { logError } from "./logger.js";
5
+ const PHASE_TIMEOUT_MS = 30 * 60 * 1000; // 30분
6
+ const MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB
7
+ export async function executeClaudePhase(config) {
8
+ const args = ["-p"];
9
+ // 시스템 프롬프트 추가 (구현/검증 규칙 주입)
10
+ if (config.systemPromptAppend) {
11
+ args.push("--append-system-prompt", config.systemPromptAppend);
12
+ }
13
+ // 도구 제한
14
+ if (config.allowedTools?.length) {
15
+ args.push("--allowedTools", ...config.allowedTools);
16
+ }
17
+ // 자율 실행 모드
18
+ if (config.skipPermissions) {
19
+ args.push("--dangerously-skip-permissions");
20
+ }
21
+ // JSON 출력
22
+ args.push("--output-format", "json");
23
+ return new Promise((resolve, reject) => {
24
+ const child = spawn("claude", args, {
25
+ cwd: config.cwd,
26
+ stdio: ["pipe", "pipe", "pipe"],
27
+ });
28
+ let settled = false;
29
+ // #5: escalation 타이머를 저장하여 close 시 정리
30
+ let escalationTimer = null;
31
+ // 타임아웃: 30분 후 강제 종료
32
+ const timer = setTimeout(() => {
33
+ if (!settled) {
34
+ child.kill("SIGTERM");
35
+ escalationTimer = setTimeout(() => {
36
+ if (!settled)
37
+ try {
38
+ child.kill("SIGKILL");
39
+ }
40
+ catch { /* already exited */ }
41
+ }, 10_000);
42
+ escalationTimer.unref();
43
+ }
44
+ }, PHASE_TIMEOUT_MS);
45
+ // stdin 에러 처리 (자식 프로세스가 즉시 종료된 경우 pipe 깨짐 방지)
46
+ child.stdin.on("error", () => { });
47
+ child.stdin.write(config.prompt);
48
+ child.stdin.end();
49
+ let stdout = "";
50
+ let stderr = "";
51
+ let truncated = false;
52
+ child.stdout.on("data", (data) => {
53
+ const remaining = MAX_BUFFER_SIZE - stdout.length;
54
+ if (remaining > 0) {
55
+ const chunk = data.toString();
56
+ stdout += remaining >= chunk.length ? chunk : chunk.slice(0, remaining);
57
+ }
58
+ else if (!truncated) {
59
+ truncated = true;
60
+ logError("stdout 버퍼 제한 초과 — 출력이 잘렸습니다.");
61
+ }
62
+ });
63
+ child.stderr.on("data", (data) => {
64
+ if (stderr.length < MAX_BUFFER_SIZE) {
65
+ stderr += data.toString();
66
+ }
67
+ });
68
+ child.on("error", (err) => {
69
+ if (!settled) {
70
+ settled = true;
71
+ clearTimeout(timer);
72
+ if (escalationTimer)
73
+ clearTimeout(escalationTimer);
74
+ reject(new Error(`claude 실행 실패: ${err.message}`));
75
+ }
76
+ });
77
+ child.on("close", (code, signal) => {
78
+ if (settled)
79
+ return;
80
+ settled = true;
81
+ clearTimeout(timer);
82
+ if (escalationTimer)
83
+ clearTimeout(escalationTimer);
84
+ if (signal === "SIGTERM" || signal === "SIGKILL") {
85
+ reject(new Error(`claude 프로세스가 타임아웃으로 종료되었습니다 (${PHASE_TIMEOUT_MS / 60_000}분).`));
86
+ return;
87
+ }
88
+ // #12: 비정상 종료 + 출력 없음 → 에러로 처리
89
+ if (code !== 0) {
90
+ logError(`claude 종료 코드: ${code}`);
91
+ if (stderr)
92
+ logError(`stderr: ${stderr.slice(0, 500)}`);
93
+ if (!stdout.trim()) {
94
+ reject(new Error(`claude가 출력 없이 종료되었습니다 (종료 코드: ${code}).`));
95
+ return;
96
+ }
97
+ }
98
+ try {
99
+ const parsed = JSON.parse(stdout);
100
+ resolve({
101
+ text: parsed.result ?? stdout,
102
+ sessionId: parsed.session_id ?? "",
103
+ });
104
+ }
105
+ catch {
106
+ // JSON 파싱 실패 시 원시 텍스트 반환
107
+ resolve({
108
+ text: stdout || stderr,
109
+ sessionId: "",
110
+ });
111
+ }
112
+ });
113
+ });
114
+ }
115
+ // claude CLI 존재 여부 확인
116
+ const CLI_CHECK_TIMEOUT_MS = 10_000;
117
+ export async function checkClaudeCLI() {
118
+ return new Promise((resolve) => {
119
+ let done = false;
120
+ const child = spawn("claude", ["--version"], {
121
+ stdio: ["ignore", "pipe", "pipe"],
122
+ });
123
+ const timer = setTimeout(() => {
124
+ if (!done) {
125
+ done = true;
126
+ child.kill("SIGTERM");
127
+ resolve(false);
128
+ }
129
+ }, CLI_CHECK_TIMEOUT_MS);
130
+ child.on("close", (code) => {
131
+ if (!done) {
132
+ done = true;
133
+ clearTimeout(timer);
134
+ resolve(code === 0);
135
+ }
136
+ });
137
+ child.on("error", () => {
138
+ if (!done) {
139
+ done = true;
140
+ clearTimeout(timer);
141
+ resolve(false);
142
+ }
143
+ });
144
+ });
145
+ }
146
+ //# sourceMappingURL=executor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.js","sourceRoot":"","sources":["../src/executor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export interface ExternalHelpResult {
2
+ source: string;
3
+ advice: string;
4
+ }
5
+ export declare function getExternalHelp(problem: string, cwd: string): Promise<ExternalHelpResult | null>;
@@ -0,0 +1,78 @@
1
+ // 외부 AI 도움: 연속 실패 시 gemini/codex CLI로 조언 요청
2
+ import { spawn } from "node:child_process";
3
+ import { logInfo, logWarn } from "./logger.js";
4
+ const HELP_TIMEOUT_MS = 5 * 60 * 1000; // 5분
5
+ const MAX_OUTPUT_SIZE = 50_000; // 50KB
6
+ export async function getExternalHelp(problem, cwd) {
7
+ // gemini CLI 시도
8
+ const geminiResult = await tryGemini(problem, cwd);
9
+ if (geminiResult)
10
+ return geminiResult;
11
+ // codex CLI 시도
12
+ const codexResult = await tryCodex(problem, cwd);
13
+ if (codexResult)
14
+ return codexResult;
15
+ logWarn("외부 AI 도움을 받을 수 없습니다 (gemini, codex 모두 실패).");
16
+ return null;
17
+ }
18
+ async function tryGemini(problem, cwd) {
19
+ try {
20
+ logInfo("gemini CLI로 도움 요청 중...");
21
+ const output = await runCli("gemini", ["-p", problem], cwd);
22
+ if (output)
23
+ return { source: "gemini", advice: output };
24
+ }
25
+ catch {
26
+ logWarn("gemini CLI 실행 실패.");
27
+ }
28
+ return null;
29
+ }
30
+ async function tryCodex(problem, cwd) {
31
+ try {
32
+ logInfo("codex CLI로 도움 요청 중...");
33
+ const output = await runCli("codex", ["exec", problem], cwd);
34
+ if (output)
35
+ return { source: "codex", advice: output };
36
+ }
37
+ catch {
38
+ logWarn("codex CLI 실행 실패.");
39
+ }
40
+ return null;
41
+ }
42
+ function runCli(command, args, cwd) {
43
+ return new Promise((resolve) => {
44
+ let settled = false;
45
+ const child = spawn(command, args, {
46
+ cwd,
47
+ stdio: ["ignore", "pipe", "pipe"],
48
+ });
49
+ const timer = setTimeout(() => {
50
+ if (!settled) {
51
+ settled = true;
52
+ child.kill("SIGTERM");
53
+ resolve(null);
54
+ }
55
+ }, HELP_TIMEOUT_MS);
56
+ let stdout = "";
57
+ child.stdout.on("data", (data) => {
58
+ if (stdout.length < MAX_OUTPUT_SIZE) {
59
+ stdout += data.toString();
60
+ }
61
+ });
62
+ child.on("error", () => {
63
+ if (!settled) {
64
+ settled = true;
65
+ clearTimeout(timer);
66
+ resolve(null);
67
+ }
68
+ });
69
+ child.on("close", (code) => {
70
+ if (!settled) {
71
+ settled = true;
72
+ clearTimeout(timer);
73
+ resolve(code === 0 && stdout.trim() ? stdout.trim() : null);
74
+ }
75
+ });
76
+ });
77
+ }
78
+ //# sourceMappingURL=external-help.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"external-help.js","sourceRoot":"","sources":["../src/external-help.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import { Phase } from "./types.js";
2
+ export declare function logPhaseStart(phase: Phase, iteration: number): void;
3
+ export declare function logPhaseResult(phase: Phase, success: boolean): void;
4
+ export declare function logInfo(message: string): void;
5
+ export declare function logWarn(message: string): void;
6
+ export declare function logError(message: string): void;
7
+ export declare function logSuccess(message: string): void;
package/dist/logger.js ADDED
@@ -0,0 +1,49 @@
1
+ // 콘솔 출력 포맷
2
+ import { Phase } from "./types.js";
3
+ // 비TTY(파일 리다이렉트, 플러그인 모드 등)에서 ANSI 코드 비활성화
4
+ const USE_COLORS = process.stdout.isTTY ?? false;
5
+ const COLORS = USE_COLORS
6
+ ? {
7
+ reset: "\x1b[0m",
8
+ bold: "\x1b[1m",
9
+ dim: "\x1b[2m",
10
+ green: "\x1b[32m",
11
+ yellow: "\x1b[33m",
12
+ blue: "\x1b[34m",
13
+ cyan: "\x1b[36m",
14
+ red: "\x1b[31m",
15
+ }
16
+ : {
17
+ reset: "", bold: "", dim: "", green: "", yellow: "",
18
+ blue: "", cyan: "", red: "",
19
+ };
20
+ const PHASE_LABELS = {
21
+ [Phase.CLARIFY]: "요구사항 확인",
22
+ [Phase.IMPLEMENT]: "구현",
23
+ [Phase.VERIFY]: "검증",
24
+ [Phase.DONE]: "완료",
25
+ };
26
+ export function logPhaseStart(phase, iteration) {
27
+ const label = PHASE_LABELS[phase];
28
+ console.log(`\n${COLORS.bold}${COLORS.cyan}[soda]${COLORS.reset} ` +
29
+ `${COLORS.blue}${label}${COLORS.reset} ` +
30
+ `${COLORS.dim}(반복 #${iteration})${COLORS.reset}`);
31
+ }
32
+ export function logPhaseResult(phase, success) {
33
+ const label = PHASE_LABELS[phase];
34
+ const icon = success ? `${COLORS.green}✓` : `${COLORS.red}✗`;
35
+ console.log(`${COLORS.bold}${icon} ${label}${COLORS.reset}`);
36
+ }
37
+ export function logInfo(message) {
38
+ console.log(`${COLORS.cyan}[soda]${COLORS.reset} ${message}`);
39
+ }
40
+ export function logWarn(message) {
41
+ console.log(`${COLORS.yellow}[soda 경고]${COLORS.reset} ${message}`);
42
+ }
43
+ export function logError(message) {
44
+ console.error(`${COLORS.red}[soda 오류]${COLORS.reset} ${message}`);
45
+ }
46
+ export function logSuccess(message) {
47
+ console.log(`${COLORS.green}${COLORS.bold}[soda]${COLORS.reset} ${COLORS.green}${message}${COLORS.reset}`);
48
+ }
49
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,WAAW;AAEX,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,2CAA2C;AAC3C,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC;AAEjD,MAAM,MAAM,GAAG,UAAU;IACvB,CAAC,CAAC;QACE,KAAK,EAAE,SAAS;QAChB,IAAI,EAAE,SAAS;QACf,GAAG,EAAE,SAAS;QACd,KAAK,EAAE,UAAU;QACjB,MAAM,EAAE,UAAU;QAClB,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE,UAAU;QAChB,GAAG,EAAE,UAAU;KAChB;IACH,CAAC,CAAC;QACE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE;QACnD,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE;KAC5B,CAAC;AAEN,MAAM,YAAY,GAA0B;IAC1C,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,SAAS;IAC1B,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,IAAI;IACvB,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI;IACpB,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI;CACnB,CAAC;AAEF,MAAM,UAAU,aAAa,CAAC,KAAY,EAAE,SAAiB;IAC3D,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IAClC,OAAO,CAAC,GAAG,CACT,KAAK,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,SAAS,MAAM,CAAC,KAAK,GAAG;QACpD,GAAG,MAAM,CAAC,IAAI,GAAG,KAAK,GAAG,MAAM,CAAC,KAAK,GAAG;QACxC,GAAG,MAAM,CAAC,GAAG,QAAQ,SAAS,IAAI,MAAM,CAAC,KAAK,EAAE,CACnD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAY,EAAE,OAAgB;IAC3D,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG,GAAG,CAAC;IAC7D,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,GAAG,IAAI,IAAI,KAAK,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,OAAe;IACrC,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,SAAS,MAAM,CAAC,KAAK,IAAI,OAAO,EAAE,CAAC,CAAC;AAChE,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,OAAe;IACrC,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,YAAY,MAAM,CAAC,KAAK,IAAI,OAAO,EAAE,CAAC,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,OAAO,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,GAAG,YAAY,MAAM,CAAC,KAAK,IAAI,OAAO,EAAE,CAAC,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,OAAe;IACxC,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,IAAI,SAAS,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,GAAG,OAAO,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;AAC7G,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { type Limits } from "./types.js";
2
+ interface OrchestratorOptions {
3
+ goal: string;
4
+ workingDir: string;
5
+ resume: boolean;
6
+ limits: Limits;
7
+ pluginMode?: boolean;
8
+ getUserInput?: (clarifyOutput: string, workingDir: string) => Promise<string | null>;
9
+ }
10
+ export declare function run(options: OrchestratorOptions): Promise<void>;
11
+ export {};