@pollit/twin-dev-bot 0.0.1 → 0.0.2

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.
@@ -1,210 +0,0 @@
1
- import { spawn, execSync } from "child_process";
2
- import { EventEmitter } from "events";
3
- import { createLogger } from "./logger.js";
4
- const log = createLogger("claude-runner");
5
- export class ClaudeRunner extends EventEmitter {
6
- options;
7
- sessionId = null;
8
- process = null;
9
- // 중복 방지용 상태
10
- /** 이미 처리한 tool_use ID를 추적 */
11
- processedToolUseIds = new Set();
12
- /** init 이벤트 발생 여부 */
13
- initEmitted = false;
14
- /** result 이벤트 발생 여부 */
15
- resultEmitted = false;
16
- /** stderr 출력 누적 버퍼 */
17
- stderrLines = [];
18
- constructor(options) {
19
- super();
20
- this.options = options;
21
- this.sessionId = options.sessionId || null;
22
- }
23
- get currentSessionId() {
24
- return this.sessionId;
25
- }
26
- /** 누적된 stderr 출력을 반환 */
27
- get stderrOutput() {
28
- return this.stderrLines.join("").trim();
29
- }
30
- run() {
31
- const args = [
32
- "-p", this.options.prompt,
33
- "--output-format", "stream-json",
34
- "--verbose",
35
- "--dangerously-skip-permissions",
36
- ];
37
- // 세션 ID가 있으면 --resume 추가
38
- if (this.options.sessionId) {
39
- args.unshift("--resume", this.options.sessionId);
40
- }
41
- log.info("Running Claude", {
42
- directory: this.options.directory,
43
- prompt: this.options.prompt,
44
- sessionId: this.options.sessionId || "new",
45
- });
46
- this.process = spawn("claude", args, {
47
- cwd: this.options.directory,
48
- stdio: ["ignore", "pipe", "pipe"], // stdin은 ignore로 설정 (pipe로 하면 Claude가 입력 대기 상태가 됨)
49
- shell: process.platform === "win32", // Windows에서 .cmd/.exe PATH 해석에 필요
50
- });
51
- let buffer = "";
52
- this.process.stdout?.on("data", (data) => {
53
- const chunk = data.toString();
54
- buffer += chunk;
55
- // 줄 단위로 파싱
56
- const lines = buffer.split("\n");
57
- buffer = lines.pop() || "";
58
- for (const line of lines) {
59
- const trimmed = line.trim();
60
- if (trimmed) {
61
- this.handleLine(trimmed);
62
- }
63
- }
64
- });
65
- this.process.stderr?.on("data", (data) => {
66
- const text = data.toString();
67
- log.debug("Claude stderr", { text });
68
- this.stderrLines.push(text);
69
- });
70
- this.process.on("error", (err) => {
71
- log.error("Claude process error", { error: err.message });
72
- this.emit("error", err);
73
- });
74
- this.process.on("exit", (code) => {
75
- log.info("Claude process exited", { code });
76
- // 남은 버퍼 처리
77
- if (buffer.trim()) {
78
- this.handleLine(buffer.trim());
79
- }
80
- this.emit("exit", code);
81
- });
82
- }
83
- handleLine(line) {
84
- log.debug("Claude output", { line });
85
- try {
86
- const event = JSON.parse(line);
87
- this.handleEvent(event);
88
- }
89
- catch {
90
- // Non-JSON output from Claude CLI (e.g., progress messages)
91
- log.debug("Non-JSON line from Claude (ignored)", { line: line.slice(0, 200) });
92
- }
93
- }
94
- handleEvent(event) {
95
- log.debug("Claude event", { type: event.type });
96
- switch (event.type) {
97
- case "system":
98
- if ("subtype" in event && event.subtype === "init") {
99
- // 중복 init 방지
100
- if (this.initEmitted) {
101
- log.debug("Skipping duplicate init event");
102
- break;
103
- }
104
- this.initEmitted = true;
105
- const initEvent = event;
106
- this.sessionId = initEvent.session_id;
107
- this.emit("init", {
108
- sessionId: initEvent.session_id,
109
- model: initEvent.model,
110
- });
111
- }
112
- break;
113
- case "assistant":
114
- const msg = event.message;
115
- if (msg?.content) {
116
- for (const block of msg.content) {
117
- if (block.type === "text" && block.text) {
118
- this.emit("text", { text: block.text });
119
- }
120
- else if (block.type === "tool_use" && block.name) {
121
- // tool_use 블록 수신 시 전체 구조를 로그로 기록 (문제 진단용)
122
- log.debug("tool_use block received", {
123
- id: block.id,
124
- name: block.name,
125
- hasId: !!block.id,
126
- blockKeys: Object.keys(block),
127
- });
128
- // 중복 tool_use 필터링 (id가 있는 경우)
129
- const toolUseId = block.id;
130
- if (toolUseId && this.processedToolUseIds.has(toolUseId)) {
131
- log.info("Skipping duplicate tool_use", { toolUseId, toolName: block.name });
132
- continue;
133
- }
134
- if (toolUseId) {
135
- this.processedToolUseIds.add(toolUseId);
136
- }
137
- const toolEvent = {
138
- toolName: block.name,
139
- input: block.input || {},
140
- };
141
- this.emit("toolUse", toolEvent);
142
- // AskUserQuestion 처리 흐름:
143
- // ─────────────────────────────────────────────────────────────
144
- // 1. Claude CLI가 AskUserQuestion tool_use를 출력
145
- // 2. 여기서 "askUser" 이벤트를 emit
146
- // 3. claude-runner-setup.ts의 핸들러가 Slack 스레드로 질문 블록 전송
147
- // 4. Slack 전송 완료 후 runner.kill()을 호출하여 프로세스를 종료
148
- // - stdin이 "ignore"라서 Claude CLI가 응답을 받을 수 없음
149
- // - 종료하지 않으면 Claude가 에러를 받고 같은 질문을 반복함
150
- // 5. 사용자가 Slack에서 버튼 클릭 또는 직접 입력으로 응답
151
- // 6. question-handlers.ts에서 --resume으로 새 프로세스를 시작
152
- // ─────────────────────────────────────────────────────────────
153
- if (block.name === "AskUserQuestion") {
154
- this.emit("askUser", {
155
- input: block.input,
156
- });
157
- }
158
- // ExitPlanMode 처리:
159
- // stdin이 "ignore"라 CLI가 사용자 승인을 받을 수 없어 실패함.
160
- // askUser와 동일한 패턴으로 프로세스 kill → resume으로 처리.
161
- if (block.name === "ExitPlanMode") {
162
- this.emit("exitPlanMode", {});
163
- }
164
- }
165
- }
166
- }
167
- break;
168
- case "result":
169
- // 중복 result 방지
170
- if (this.resultEmitted) {
171
- log.debug("Skipping duplicate result event");
172
- break;
173
- }
174
- this.resultEmitted = true;
175
- const resultEvent = event;
176
- this.emit("result", {
177
- result: resultEvent.result,
178
- costUsd: resultEvent.total_cost_usd,
179
- });
180
- break;
181
- }
182
- }
183
- kill() {
184
- if (this.process) {
185
- if (process.platform === "win32" && this.process.pid) {
186
- // Windows에서 shell: true 사용 시 프로세스 트리 전체를 종료해야 함
187
- try {
188
- execSync(`taskkill /pid ${this.process.pid} /T /F`, { stdio: "ignore" });
189
- }
190
- catch {
191
- this.process.kill();
192
- }
193
- }
194
- else {
195
- try {
196
- this.process.kill();
197
- }
198
- catch {
199
- // Process already exited - ignore ESRCH
200
- }
201
- }
202
- this.process = null;
203
- }
204
- }
205
- }
206
- export function runClaude(options) {
207
- const runner = new ClaudeRunner(options);
208
- runner.run();
209
- return runner;
210
- }
package/dist/config.d.ts DELETED
@@ -1,9 +0,0 @@
1
- export declare const config: {
2
- readonly slack: {
3
- botToken: string;
4
- appToken: string;
5
- };
6
- readonly baseDir: string;
7
- readonly inactivityTimeoutMinutes: number;
8
- readonly inactivityTimeoutMs: number;
9
- };
package/dist/config.js DELETED
@@ -1,49 +0,0 @@
1
- import dotenv from "dotenv";
2
- import { ENV_FILE } from "./paths.js";
3
- import { createLogger } from "./logger.js";
4
- import { expandTilde, getDefaultBaseDir } from "./platform.js";
5
- const log = createLogger("config");
6
- let _initialized = false;
7
- function ensureInit() {
8
- if (_initialized)
9
- return;
10
- _initialized = true;
11
- dotenv.config({ path: ENV_FILE, override: true });
12
- const missing = [];
13
- if (!process.env.SLACK_BOT_TOKEN)
14
- missing.push("SLACK_BOT_TOKEN");
15
- if (!process.env.SLACK_APP_TOKEN)
16
- missing.push("SLACK_APP_TOKEN");
17
- if (missing.length > 0) {
18
- throw new Error(`Missing required environment variables: ${missing.join(", ")}. ` +
19
- `Please configure them in your .env file.`);
20
- }
21
- }
22
- export const config = {
23
- get slack() {
24
- ensureInit();
25
- return {
26
- botToken: process.env.SLACK_BOT_TOKEN,
27
- appToken: process.env.SLACK_APP_TOKEN,
28
- };
29
- },
30
- get baseDir() {
31
- ensureInit();
32
- const raw = process.env.TWINDEVBOT_BASE_DIR;
33
- return raw ? expandTilde(raw) : getDefaultBaseDir();
34
- },
35
- get inactivityTimeoutMinutes() {
36
- ensureInit();
37
- const DEFAULT_MINUTES = 30;
38
- const raw = process.env.INACTIVITY_TIMEOUT_MINUTES;
39
- const parsed = parseInt(raw || String(DEFAULT_MINUTES), 10);
40
- if (isNaN(parsed) || parsed < 1) {
41
- log.warn(`Invalid INACTIVITY_TIMEOUT_MINUTES="${raw}", using default ${DEFAULT_MINUTES} minutes`);
42
- return DEFAULT_MINUTES;
43
- }
44
- return parsed;
45
- },
46
- get inactivityTimeoutMs() {
47
- return this.inactivityTimeoutMinutes * 60 * 1000;
48
- },
49
- };
@@ -1,53 +0,0 @@
1
- /**
2
- * 대화 파일 관리 모듈
3
- *
4
- * 각 프로젝트의 작업 디렉토리에 conversation 폴더를 생성하고 대화를 관리합니다.
5
- * 파일 경로:
6
- * - {directory}/conversation/dialogue.json - 처리 완료된 메시지 기록 (SAVE_DIALOGUE=true 시)
7
- * - {directory}/conversation/claude-output-raw.jsonl - Claude CLI 원본 출력 (SAVE_CLAUDE_OUTPUT_RAW=true 시)
8
- */
9
- import type { ConversationFile, Message, MessageType, MessageContent } from "./types/conversation.js";
10
- /**
11
- * 작업 디렉토리에서 conversation 디렉토리 경로 반환
12
- */
13
- export declare function getConversationDir(directory: string): string;
14
- /**
15
- * dialogue.json 파일 경로 반환
16
- */
17
- export declare function getDialoguePath(directory: string): string;
18
- /**
19
- * claude-output-raw.jsonl 파일 경로 반환
20
- */
21
- export declare function getRawOutputPath(directory: string): string;
22
- /**
23
- * conversation 디렉토리가 존재하는지 확인하고, 없으면 생성
24
- */
25
- export declare function ensureConversationDir(directory: string): void;
26
- /**
27
- * 대화 파일 로드
28
- */
29
- export declare function loadConversation(directory: string): ConversationFile | null;
30
- /**
31
- * 대화 파일 저장
32
- */
33
- export declare function saveConversation(directory: string, conversation: ConversationFile): boolean;
34
- /**
35
- * 새 대화 파일 생성
36
- */
37
- export declare function createConversation(projectName: string, directory: string, slackChannelId: string): ConversationFile | null;
38
- /**
39
- * 대화 파일 로드 또는 생성
40
- */
41
- export declare function getOrCreateConversation(projectName: string, directory: string, slackChannelId: string): ConversationFile | null;
42
- /**
43
- * 처리 완료된 메시지 기록
44
- *
45
- * dialogue.json에는 처리가 성공적으로 끝난 것만 기록됩니다.
46
- * - Claude 메시지: Slack 전송 성공 후 기록
47
- * - 사용자 메시지: Claude 입력 성공 후 기록
48
- */
49
- export declare function recordMessage(directory: string, type: MessageType, content: MessageContent): Message | null;
50
- /**
51
- * Claude 세션 ID 업데이트
52
- */
53
- export declare function updateClaudeSessionId(directory: string, sessionId: string): boolean;
@@ -1,173 +0,0 @@
1
- /**
2
- * 대화 파일 관리 모듈
3
- *
4
- * 각 프로젝트의 작업 디렉토리에 conversation 폴더를 생성하고 대화를 관리합니다.
5
- * 파일 경로:
6
- * - {directory}/conversation/dialogue.json - 처리 완료된 메시지 기록 (SAVE_DIALOGUE=true 시)
7
- * - {directory}/conversation/claude-output-raw.jsonl - Claude CLI 원본 출력 (SAVE_CLAUDE_OUTPUT_RAW=true 시)
8
- */
9
- import { writeFileSync, readFileSync, existsSync, mkdirSync } from "fs";
10
- import { join } from "path";
11
- import { randomUUID } from "crypto";
12
- import { createLogger } from "./logger.js";
13
- import { config } from "./config.js";
14
- const log = createLogger("conversation-store");
15
- /**
16
- * 작업 디렉토리에서 conversation 디렉토리 경로 반환
17
- */
18
- export function getConversationDir(directory) {
19
- return join(directory, "conversation");
20
- }
21
- /**
22
- * dialogue.json 파일 경로 반환
23
- */
24
- export function getDialoguePath(directory) {
25
- return join(getConversationDir(directory), "dialogue.json");
26
- }
27
- /**
28
- * claude-output-raw.jsonl 파일 경로 반환
29
- */
30
- export function getRawOutputPath(directory) {
31
- return join(getConversationDir(directory), "claude-output-raw.jsonl");
32
- }
33
- /**
34
- * conversation 디렉토리가 존재하는지 확인하고, 없으면 생성
35
- */
36
- export function ensureConversationDir(directory) {
37
- const conversationDir = getConversationDir(directory);
38
- if (!existsSync(conversationDir)) {
39
- mkdirSync(conversationDir, { recursive: true });
40
- log.debug("Created conversation directory", { dir: conversationDir });
41
- }
42
- }
43
- /**
44
- * 대화 파일 로드
45
- */
46
- export function loadConversation(directory) {
47
- if (!config.dialogue.save) {
48
- return null;
49
- }
50
- const filePath = getDialoguePath(directory);
51
- if (!existsSync(filePath)) {
52
- return null;
53
- }
54
- try {
55
- const content = readFileSync(filePath, "utf-8");
56
- return JSON.parse(content);
57
- }
58
- catch (error) {
59
- log.error("Failed to load dialogue file", { directory, error });
60
- return null;
61
- }
62
- }
63
- /**
64
- * 대화 파일 저장
65
- */
66
- export function saveConversation(directory, conversation) {
67
- if (!config.dialogue.save) {
68
- return true; // 저장 비활성화 시 성공으로 처리
69
- }
70
- ensureConversationDir(directory);
71
- const filePath = getDialoguePath(directory);
72
- try {
73
- conversation.updatedAt = new Date().toISOString();
74
- writeFileSync(filePath, JSON.stringify(conversation, null, 2));
75
- log.debug("Dialogue saved", { directory });
76
- return true;
77
- }
78
- catch (error) {
79
- log.error("Failed to save dialogue file", { directory, error });
80
- return false;
81
- }
82
- }
83
- /**
84
- * 새 대화 파일 생성
85
- */
86
- export function createConversation(projectName, directory, slackChannelId) {
87
- if (!config.dialogue.save) {
88
- return null;
89
- }
90
- ensureConversationDir(directory);
91
- const now = new Date().toISOString();
92
- const conversation = {
93
- projectName,
94
- directory,
95
- slackChannelId,
96
- claudeSessionId: null,
97
- createdAt: now,
98
- updatedAt: now,
99
- messages: [],
100
- };
101
- saveConversation(directory, conversation);
102
- log.info("Created new dialogue", { projectName, directory });
103
- return conversation;
104
- }
105
- /**
106
- * 대화 파일 로드 또는 생성
107
- */
108
- export function getOrCreateConversation(projectName, directory, slackChannelId) {
109
- if (!config.dialogue.save) {
110
- return null;
111
- }
112
- const existing = loadConversation(directory);
113
- if (existing) {
114
- // 기존 대화가 있으면 채널 ID 업데이트 (다른 채널에서 시작할 수 있음)
115
- existing.slackChannelId = slackChannelId;
116
- saveConversation(directory, existing);
117
- return existing;
118
- }
119
- return createConversation(projectName, directory, slackChannelId);
120
- }
121
- /**
122
- * 메시지 ID 생성
123
- */
124
- function generateMessageId() {
125
- return randomUUID();
126
- }
127
- /**
128
- * 처리 완료된 메시지 기록
129
- *
130
- * dialogue.json에는 처리가 성공적으로 끝난 것만 기록됩니다.
131
- * - Claude 메시지: Slack 전송 성공 후 기록
132
- * - 사용자 메시지: Claude 입력 성공 후 기록
133
- */
134
- export function recordMessage(directory, type, content) {
135
- if (!config.dialogue.save) {
136
- return null; // 저장 비활성화 시 건너뜀
137
- }
138
- let conversation = loadConversation(directory);
139
- if (!conversation) {
140
- log.error("Dialogue not found", { directory });
141
- return null;
142
- }
143
- const message = {
144
- id: generateMessageId(),
145
- type,
146
- timestamp: new Date().toISOString(),
147
- processed: true, // 기록 시점에 이미 처리 완료
148
- content,
149
- };
150
- conversation.messages.push(message);
151
- saveConversation(directory, conversation);
152
- log.debug("Message recorded", {
153
- directory,
154
- messageId: message.id,
155
- type,
156
- });
157
- return message;
158
- }
159
- /**
160
- * Claude 세션 ID 업데이트
161
- */
162
- export function updateClaudeSessionId(directory, sessionId) {
163
- if (!config.dialogue.save) {
164
- return true; // 저장 비활성화 시 성공으로 처리
165
- }
166
- const conversation = loadConversation(directory);
167
- if (!conversation) {
168
- return false;
169
- }
170
- conversation.claudeSessionId = sessionId;
171
- saveConversation(directory, conversation);
172
- return true;
173
- }
package/dist/i18n/ko.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare const ko: Record<string, string>;
package/dist/i18n/ko.js DELETED
@@ -1,141 +0,0 @@
1
- export const ko = {
2
- // question-blocks.ts
3
- "question.header": "질문",
4
- "question.headerCompleted": "질문 (답변 완료)",
5
- "question.submitSelection": "선택 완료",
6
- "question.textInput": "✏️ 직접 입력",
7
- "question.currentSelection": "_현재 선택: {{labels}}_",
8
- "question.selectHint": "_옵션을 선택한 후 '선택 완료' 버튼을 눌러주세요_",
9
- "question.truncatedOptions": "_{{total}}개 중 {{shown}}개 옵션만 표시됩니다. 나머지는 '직접 입력'을 이용해주세요._",
10
- // question-handlers.ts (modal)
11
- "modal.title": "답변 입력",
12
- "modal.submit": "전송",
13
- "modal.cancel": "취소",
14
- "modal.prompt": "답변을 입력하세요:",
15
- "modal.label": "답변",
16
- "modal.placeholder": "답변을 입력하세요...",
17
- // question-handlers.ts (session)
18
- "session.expired": "세션이 만료되었습니다. `/twindevbot goto 프로젝트명`으로 새 세션을 시작하세요.",
19
- "multiSelect.noneSelected": "옵션을 하나 이상 선택한 후 '선택 완료' 버튼을 눌러주세요.",
20
- // claude-command.ts
21
- "command.gotoUsage": ":warning: 사용법: `/twindevbot goto <디렉토리명>`\n예: `/twindevbot goto my-project`",
22
- "command.dirNotFound": ":x: `{{dirName}}` 디렉토리가 존재하지 않습니다.\n`/twindevbot new {{dirName}} --empty` 또는 `/twindevbot new {{dirName}} --template <templateName>` 명령을 먼저 실행해주세요.\n\n사용 가능한 템플릿:\n{{templates}}",
23
- "command.sessionStarted": ":file_folder: `{{dirName}}` 디렉토리에서 새 세션이 시작됐습니다.\n스레드에서 작업을 진행하세요.",
24
- "command.gotoSuccess": ":file_folder: `{{dirName}}`(으)로 이동했습니다.\n무슨 작업을 시작할까요?",
25
- "command.autopilotNotice": "(현재 autopilot 모드입니다.\n한번의 스레드 메시지 입력만으로 작업이 시작~완료되고\n작업 도중 발생하는 모든 질문들은 자동 응답 처리됩니다.\n따라서 좋은 결과를 위해 최대한 친절하고 자세한 내용의 메시지로\n명령을 내려주세요.)",
26
- "command.newUsage": ":warning: 사용법:\n`/twindevbot new <디렉토리명> --empty`\n`/twindevbot new <디렉토리명> --template <templateName>`\n\n사용 가능한 템플릿:\n{{templates}}",
27
- "command.newOptionsRequired": ":warning: 옵션을 지정해주세요:\n`/twindevbot new {{dirName}} --empty`\n`/twindevbot new {{dirName}} --template react`\n\n사용 가능한 템플릿:\n{{templates}}",
28
- "command.invalidDirName": ":x: `{{dirName}}`은(는) 유효하지 않은 디렉토리명입니다. 영문, 숫자, 하이픈(`-`), 밑줄(`_`), 마침표(`.`)만 사용할 수 있습니다.",
29
- "command.dirAlreadyExists": ":x: `{{dirName}}` 디렉토리가 이미 존재합니다. 다른 이름으로 디렉토리를 생성하세요.",
30
- "command.errorOccurred": ":rotating_light: 이런 에러가 발생했어요, 다시 시도해보세요.\n```{{error}}```",
31
- "command.emptyDirCreated": ":white_check_mark: `{{dirName}}` 빈 디렉토리를 생성했습니다.",
32
- "command.templateNotFound": ":x: `{{templateKey}}` 템플릿을 찾을 수 없습니다.\n\n사용 가능한 템플릿:\n{{templates}}",
33
- "command.creatingProject": ":hourglass_flowing_sand: `{{templateName}}` 프로젝트를 생성 중입니다...\n시간이 조금 걸릴 수 있습니다. 모두 완료되면 다시 답변드릴게요.\n`{{command}}`",
34
- "command.projectCreated": ":white_check_mark: `{{templateName}}` 프로젝트가 `{{dirName}}`에 생성되었습니다.",
35
- "command.autopilotInterruptConfirm": "중간에 개입하시면 autopilot 모드는 중단됩니다. 그렇게 할까요?",
36
- "command.autopilotInterruptYes": "네",
37
- "command.autopilotInterruptNo": "아니요",
38
- "command.autopilotInterruptedYes": "autopilot 모드를 중단합니다. 말씀하신 내용을 일반 모드로 작업 시작합니다.",
39
- "command.autopilotContinue": "autopilot 모드로 계속 진행합니다.",
40
- "command.normalInterruptConfirm": "아직 작업이 완료되지 않았습니다. 이전 작업을 중단하고 바로 새 작업을 시작할까요? (Not Recommended)",
41
- "command.normalInterruptYes": "예",
42
- "command.normalInterruptNo": "아니오",
43
- "command.normalInterruptedYes": "이전 작업을 중단했고 새 작업을 시작합니다.",
44
- "command.normalInterruptContinue": "이전 작업을 계속 진행합니다, 새 명령은 작업 완료 후에 내려주세요.",
45
- "command.listResult": ":file_folder: `{{baseDir}}`에 현재 다음과 같은 디렉토리들이 있습니다.\n\n{{dirList}}\n\n`/twindevbot goto <디렉토리명>`으로 작업을 시작해보세요.",
46
- "command.listEmpty": ":open_file_folder: `{{baseDir}}`에 현재 디렉토리가 없습니다.\n`/twindevbot new <디렉토리명> --empty` 또는 `/twindevbot new <디렉토리명> --template <templateName>`으로 프로젝트를 생성해보세요.",
47
- "command.baseDirNotFound": ":x: 프로젝트 부모 디렉토리(`{{baseDir}}`)를 찾을 수 없습니다.",
48
- "command.help": ":robot_face: *TwinDevBot 명령어*\n언제 어디서든 Claude Code로 개발하세요.\n\n━━━━━━━━━━━━━━━━━━━━\n\n:rocket: *프로젝트 이동*\n`/twindevbot goto <디렉토리명>`\n`/twindevbot goto <디렉토리명> --autopilot`\n\n━━━━━━━━━━━━━━━━━━━━\n\n:mag: *프로젝트 목록*\n`/twindevbot list`\n\n━━━━━━━━━━━━━━━━━━━━\n\n:hammer_and_wrench: *프로젝트 생성*\n`/twindevbot new <디렉토리명> --empty`\n`/twindevbot new <디렉토리명> --template <프레임워크>`\n{{templates}}\n\n━━━━━━━━━━━━━━━━━━━━\n\n:bulb: *참고사항*\n* 모든 경로는 `{{baseDir}}` 기준입니다.\n* 하나의 Claude Session 속 대화는 동일 메시지 속 스레드들로 이루어집니다.\n* 프로젝트 디렉토리 생성만큼은 `twindevbot new` 명령으로 하기보다는 본인 노트북에서 프로젝트 디렉토리를 원하는 모습으로 직접 생성하고, 각종 컨텍스트 룰(ex. `CLAUDE.md`, `.claude/rules/*`) 파일을 적용해둔 뒤, `twindevbot goto`로 작업하시기를 추천드립니다.\n\n* `--autopilot` 모드: twindevbot이 모든 질문에 자동으로 답하며 알아서 개발합니다. 가벼운 프로젝트를 작업하거나, 잠들기 전에 사용해보세요.",
49
- // slack-message.ts
50
- "slack.answered": "답변 완료",
51
- "slack.question": "질문",
52
- // progress-tracker.ts
53
- "progress.tool.Read": "파일을 읽고 있습니다",
54
- "progress.tool.Write": "파일을 작성하고 있습니다",
55
- "progress.tool.Edit": "파일을 수정하고 있습니다",
56
- "progress.tool.Bash": "명령어를 실행하고 있습니다",
57
- "progress.tool.Grep": "코드를 검색하고 있습니다",
58
- "progress.tool.Glob": "파일을 찾고 있습니다",
59
- "progress.tool.Task": "하위 작업을 처리하고 있습니다",
60
- "progress.tool.WebSearch": "웹을 검색하고 있습니다",
61
- "progress.tool.WebFetch": "웹 페이지를 가져오고 있습니다",
62
- "progress.tool.NotebookEdit": "노트북을 수정하고 있습니다",
63
- "progress.tool.default": "도구를 실행하고 있습니다",
64
- "progress.working": ":gear: 작업을 시작합니다...",
65
- "progress.completed": ":white_check_mark: 작업 완료 ({{elapsed}})",
66
- "progress.error": ":x: 오류가 발생했습니다: {{error}}",
67
- "progress.planApproved": ":thumbsup: 계획이 승인되었습니다. 구현을 시작합니다...",
68
- "progress.autopilotContinue": ":robot_face: 자동 응답 완료, 계속 진행 중... ({{elapsed}})",
69
- "progress.askUser": ":raised_hand: 질문을 전송했습니다. 답변을 기다리고 있습니다.",
70
- "progress.lessThanOneSecond": "1초 미만",
71
- "progress.seconds": "{{n}}초",
72
- "progress.minutesSeconds": "{{m}}분 {{s}}초",
73
- // claude-runner-setup.ts
74
- "runner.questionArrived": "Claude Code 질문이 도착했습니다",
75
- "runner.autopilotAnswer": ":robot_face: Autopilot: *{{answer}}* 자동 선택",
76
- "runner.errorOccurred": "오류 발생: {{error}}",
77
- "runner.claudeNotFound": "Claude CLI가 설치되어 있지 않거나 PATH에 등록되어 있지 않습니다. `npm install -g @anthropic-ai/claude-code` 명령으로 설치해주세요.",
78
- "runner.exitError": ":x: Claude 프로세스가 비정상 종료되었습니다 (코드: {{code}})",
79
- "runner.autopilotResumeFailed": ":x: Autopilot: Claude 재시작에 실패했습니다.",
80
- "runner.autopilotNoSession": ":x: Autopilot: 세션 ID를 찾을 수 없어 재시작할 수 없습니다.",
81
- "runner.inactivityTimeout": ":warning: Claude 프로세스가 30분간 응답이 없어 자동 종료되었습니다. 새 메시지를 보내 다시 시작할 수 있습니다.",
82
- "runner.planApproved": "계획이 승인되었습니다. 구현을 시작하세요.",
83
- // templates.ts
84
- "template.frontend": "*Frontend:* ",
85
- "template.backend": "*Backend:* ",
86
- // cli.ts
87
- "cli.description": "언제 어디서든 Claude Code로 개발하세요.",
88
- "cli.usage": "사용법:",
89
- "cli.commands": "명령어:",
90
- "cli.cmd.start": "서버 시작 (포그라운드)",
91
- "cli.cmd.startDaemon": "서버를 백그라운드 서비스로 등록 및 시작 (macOS launchd / Windows 작업 스케줄러)",
92
- "cli.cmd.stop": "백그라운드 서비스 중지 및 해제",
93
- "cli.cmd.status": "백그라운드 서비스 상태 확인",
94
- "cli.cmd.show": "저장된 세션 목록 조회",
95
- "cli.cmd.clear": "저장된 데이터 파일 삭제 (sessions.json, workspaces.json)",
96
- "cli.cmd.help": "도움말 표시",
97
- "cli.notes": "주의사항:",
98
- "cli.notes.daemon": "twindevbot이 실행된 상태여야 원격 작업이 가능합니다. --daemon 옵션으로 실행해두시면 매번 다시 실행할 필요가 없어요.",
99
- "cli.notes.errorLog": "twindevbot 서버에 문제가 있는 것 같다면, 에러 로그를 확인해보세요.",
100
- "cli.warning.title": "경고",
101
- "cli.warning.text": "twindevbot을 사용하면 claude가 --dangerously-skip-permissions 옵션으로 시작됩니다. 이것의 의미를 분명히 아는 분들만 twindevbot을 사용하세요. twindevbot의 코드는 GitHub에 모두 공개되어 있습니다. twindevbot 사용으로 인해 어떤 사고가 발생하더라도 아무도 책임지지 않습니다.",
102
- // setup.ts
103
- "cli.setup.banner": "twindevbot 서버 설정",
104
- "cli.setup.promptAppToken": "SLACK_APP_TOKEN을 입력하세요 (xapp-...)",
105
- "cli.setup.promptBotToken": "SLACK_BOT_TOKEN을 입력하세요 (xoxb-...)",
106
- "cli.setup.promptBaseDir": "프로젝트 디렉토리들을 만들 부모 디렉토리 경로를 작성하세요",
107
- "cli.setup.promptLang": "언어를 선택하세요",
108
- "cli.setup.required": "값을 입력해주세요.",
109
- "cli.setup.saved": "설정이 저장되었습니다: {{path}}",
110
- "cli.setup.startMessage": "서버를 시작합니다.\n 이제 슬랙에서 /twindevbot이라고 쓰고 작업해보세요.",
111
- "cli.setup.startDaemonMessage": "서버를 시작합니다. 언제나 실행되어 있도록 백그라운드 서비스로 등록합니다.\n 이제 슬랙에서 /twindevbot이라고 쓰고 작업해보세요.",
112
- "cli.daemon.plistCreated": "launchd plist 생성: {{path}}",
113
- "cli.daemon.started": "백그라운드 서비스가 시작되었습니다.",
114
- "cli.daemon.failedToStart": "서비스 시작 실패:",
115
- "cli.daemon.notInstalled": "백그라운드 서비스가 설치되어 있지 않습니다.",
116
- "cli.daemon.stopped": "백그라운드 서비스가 중지 및 해제되었습니다.",
117
- "cli.status.notInstalled": "백그라운드 서비스: 미설치",
118
- "cli.status.notInstalledHint": "'twindevbot start --daemon'으로 설치하세요.",
119
- "cli.status.running": "백그라운드 서비스: 실행 중 (PID {{pid}})",
120
- "cli.status.registered": "백그라운드 서비스: 등록됨",
121
- "cli.status.notRunning": "백그라운드 서비스: 설치되었으나 실행 중이 아닙니다",
122
- "cli.status.checkLogs": "크래시했을 수 있습니다. 로그를 확인하세요:",
123
- "cli.daemonUnsupportedPlatform": "daemon 기능(start --daemon, stop, status)은 macOS와 Windows에서만 사용할 수 있습니다. 현재 플랫폼: {{platform}}",
124
- "cli.daemon.taskCreated": "Windows 예약 작업 생성: {{name}}",
125
- "cli.unknownCommand": "알 수 없는 명령어: {{command}}",
126
- // cli.ts - show
127
- "cli.show.title": "twindevbot sessions ({{count}})",
128
- "cli.show.sessionCount": "({{count}}개 세션)",
129
- "cli.show.noSessions": "저장된 세션이 없습니다.",
130
- "cli.show.parseError": "sessions.json 파일을 파싱할 수 없습니다.",
131
- "cli.show.daysAgo": "{{n}}일 전",
132
- "cli.show.hoursAgo": "{{n}}시간 전",
133
- "cli.show.minutesAgo": "{{n}}분 전",
134
- "cli.show.justNow": "방금 전",
135
- // cli.ts - clear
136
- "cli.clear.header": "삭제할 파일:",
137
- "cli.clear.confirm": "위 파일들을 삭제하시겠습니까?",
138
- "cli.clear.noData": "삭제할 데이터 파일이 없습니다.",
139
- "cli.clear.cancelled": "취소되었습니다.",
140
- "cli.clear.done": "✔ 데이터 파일이 삭제되었습니다.",
141
- };