From 2a5f3ff010d64fe05e4893a1c39df02bb18bdde3 Mon Sep 17 00:00:00 2001 From: OSgoodYZ Date: Fri, 13 Feb 2026 10:27:46 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EC=B4=88=EA=B8=B0=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=97=90=EC=84=9C=20=ED=94=8C=EB=9E=AB=ED=8F=BC=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EB=B0=8F=20LINE=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - setupEnv()에 플랫폼 선택 메뉴 추가 (Discord/LINE/둘 다) - Discord 설정: 봇 토큰 + 유저 ID 입력 - LINE 설정: Access Token, Secret, 유저 ID, 포트 입력 - 입력값 검증 추가 (Discord ID: 17-19자리, LINE ID: U+32자) - LINE 선택 시 Cloudflare Tunnel 안내 메시지 출력 Co-Authored-By: Claude Opus 4.5 --- src/bot.ts | 149 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 130 insertions(+), 19 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index f00f9c6..f396199 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -68,6 +68,15 @@ function ask(question: string): Promise { // ── First-run .env setup ───────────────────────────────────────────── +// Validation helpers +function isValidDiscordId(id: string): boolean { + return /^\d{17,19}$/.test(id); +} + +function isValidLineUserId(id: string): boolean { + return /^U[a-f0-9]{32}$/i.test(id); +} + async function setupEnv(): Promise { const envPath = path.join(process.cwd(), ".env"); if (fs.existsSync(envPath)) return; @@ -80,35 +89,137 @@ async function setupEnv(): Promise { console.log(" .env 파일이 없습니다. 필수 정보를 입력해주세요."); console.log(); - // 1) Discord 봇 토큰 - let token: string; + // 1) Platform selection + console.log(" [1] 사용할 플랫폼을 선택하세요:"); + console.log(); + console.log(" 1) Discord만"); + console.log(" 2) LINE만"); + console.log(" 3) Discord + LINE 둘 다"); + console.log(); + + let platformChoice: number; while (true) { - token = await ask(" [1/2] Discord 봇 토큰: "); - if (token) break; - console.log(" 봇 토큰은 필수입니다. 다시 입력해주세요."); + const raw = await ask(" 선택 [1-3] (기본: 1): "); + if (raw === "") { + platformChoice = 1; + break; + } + const n = parseInt(raw, 10); + if (n >= 1 && n <= 3) { + platformChoice = n; + break; + } + console.log(" 잘못된 선택입니다. 1, 2, 3 중 하나를 입력해주세요."); } - // 2) Discord 유저 ID - let userId: string; - while (true) { - userId = await ask(" [2/2] Discord 유저 ID: "); - if (userId) break; - console.log(" 유저 ID는 필수입니다. 다시 입력해주세요."); + const useDiscord = platformChoice === 1 || platformChoice === 3; + const useLine = platformChoice === 2 || platformChoice === 3; + + const platformNames = []; + if (useDiscord) platformNames.push("Discord"); + if (useLine) platformNames.push("LINE"); + console.log(` -> ${platformNames.join(" + ")} 선택됨`); + console.log(); + + // Build env content + const envLines: string[] = []; + + // Discord settings + if (useDiscord) { + console.log(" ─── Discord 설정 ───"); + console.log(); + + // Discord bot token + let discordToken: string; + while (true) { + discordToken = await ask(" Discord 봇 토큰: "); + if (discordToken) break; + console.log(" 봇 토큰은 필수입니다. 다시 입력해주세요."); + } + + // Discord user ID + let discordUserId: string; + while (true) { + discordUserId = await ask(" Discord 유저 ID (17-19자리 숫자): "); + if (isValidDiscordId(discordUserId)) break; + console.log(" 유효하지 않은 ID입니다. 17-19자리 숫자를 입력해주세요."); + } + + envLines.push("# Discord 설정"); + envLines.push(`DISCORD_BOT_TOKEN=${discordToken}`); + envLines.push(`ALLOWED_USER_IDS=${discordUserId}`); + envLines.push(""); + console.log(); } - const envContent = [ - `DISCORD_BOT_TOKEN=${token}`, - `ALLOWED_USER_IDS=${userId}`, - `COMMAND_PREFIX=!`, - `COMMAND_TIMEOUT=30`, - `AI_CLI_TIMEOUT=300`, - ].join("\n") + "\n"; + // LINE settings + if (useLine) { + console.log(" ─── LINE 설정 ───"); + console.log(); + console.log(" LINE Developers Console에서 토큰과 시크릿을 복사하세요."); + console.log(" https://developers.line.biz/console/"); + console.log(); + + // LINE Channel Access Token + let lineToken: string; + while (true) { + lineToken = await ask(" LINE Channel Access Token: "); + if (lineToken) break; + console.log(" Access Token은 필수입니다. 다시 입력해주세요."); + } + + // LINE Channel Secret + let lineSecret: string; + while (true) { + lineSecret = await ask(" LINE Channel Secret: "); + if (lineSecret) break; + console.log(" Channel Secret은 필수입니다. 다시 입력해주세요."); + } + // LINE User ID + let lineUserId: string; + while (true) { + lineUserId = await ask(" LINE 유저 ID (U로 시작하는 33자): "); + if (isValidLineUserId(lineUserId)) break; + console.log(" 유효하지 않은 LINE ID입니다. U로 시작하는 33자 문자열을 입력해주세요."); + } + + // LINE Webhook Port + const portRaw = await ask(" LINE 웹훅 포트 (기본: 3000): "); + const linePort = portRaw || "3000"; + + envLines.push("# LINE 설정"); + envLines.push(`LINE_CHANNEL_ACCESS_TOKEN=${lineToken}`); + envLines.push(`LINE_CHANNEL_SECRET=${lineSecret}`); + envLines.push(`ALLOWED_LINE_USER_IDS=${lineUserId}`); + envLines.push(`LINE_WEBHOOK_PORT=${linePort}`); + envLines.push(""); + + console.log(); + console.log(" ─── LINE 웹훅 안내 ───"); + console.log(); + console.log(" LINE 봇은 웹훅 URL이 필요합니다."); + console.log(" 로컬 테스트: Cloudflare Tunnel 사용 (무료)"); + console.log(); + console.log(" 1. 설치: winget install cloudflare.cloudflared"); + console.log(` 2. 실행: cloudflared tunnel --url http://localhost:${linePort}`); + console.log(" 3. 표시된 URL + /webhook 을 LINE Developers에 등록"); + console.log(); + } + + // Common settings + envLines.push("# 공통 설정"); + envLines.push("COMMAND_PREFIX=!"); + envLines.push("COMMAND_TIMEOUT=30"); + envLines.push("AI_CLI_TIMEOUT=300"); + + const envContent = envLines.join("\n") + "\n"; fs.writeFileSync(envPath, envContent, "utf-8"); reloadConfig(); - console.log(); + console.log("=".repeat(48)); console.log(" .env 파일이 생성되었습니다!"); + console.log("=".repeat(48)); console.log(); } From 96e73824e0c587a6e1869c2f5354ca1e0fe1291e Mon Sep 17 00:00:00 2001 From: OSgoodYZ Date: Fri, 13 Feb 2026 10:55:49 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EC=9B=B9=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=20=EC=84=A4=EC=A0=95=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/setup/setupPage.html: 예쁜 GUI 설정 페이지 - 플랫폼 선택 토글 (Discord/LINE) - Discord/LINE 설정 입력 폼 - AI CLI 도구 선택 - 작업 디렉토리 설정 - 실시간 입력 검증 - Discord/LINE 개발자 콘솔 링크 제공 - src/setup/setupServer.ts: 로컬 웹 서버 - .env 없을 때 자동으로 브라우저 오픈 - /api/setup 엔드포인트로 설정 저장 - bot.ts: 웹 설정 서버 연동 - .env 없으면 웹 설정 페이지 자동 오픈 - 웹 설정 실패 시 터미널 설정으로 폴백 Co-Authored-By: Claude Opus 4.5 --- src/bot.ts | 41 ++- src/setup/setupPage.html | 637 +++++++++++++++++++++++++++++++++++++++ src/setup/setupServer.ts | 180 +++++++++++ 3 files changed, 856 insertions(+), 2 deletions(-) create mode 100644 src/setup/setupPage.html create mode 100644 src/setup/setupServer.ts diff --git a/src/bot.ts b/src/bot.ts index f396199..cba3706 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -15,6 +15,7 @@ import { APPLICATION_ID, SLASH_COMMAND_GUILD_ID, LINE_CHANNEL_ACCESS_TOKEN, LINE_CHANNEL_SECRET, } from "./config.js"; +import { startSetupServer, envExists } from "./setup/setupServer.js"; import { LineBotServer } from "./lineBot.js"; import type { BotClient, PrefixCommand, CommandContext } from "./types.js"; import type { ISessionManager } from "./sessions/types.js"; @@ -399,7 +400,35 @@ export function createSession(cliName: string, cwd: string): ISessionManager { // ── Main ───────────────────────────────────────────────────────────── async function main(): Promise { - await setupEnv(); + let cliName: string; + let workingDir: string; + + // Check if .env exists - if not, open web setup page + if (!envExists()) { + console.log(); + console.log(" .env 파일이 없습니다. 웹 설정 페이지를 엽니다..."); + + try { + const setupResult = await startSetupServer(5000); + reloadConfig(); + cliName = setupResult.cliName; + workingDir = setupResult.workingDir || process.cwd(); + } catch (err: any) { + // Fallback to terminal setup if web setup fails + console.log(); + console.log(" 웹 설정을 사용할 수 없습니다. 터미널 설정으로 전환합니다..."); + console.log(); + await setupEnv(); + const result = await startupSetup(); + cliName = result.cliName; + workingDir = result.workingDir; + } + } else { + // .env exists, use terminal setup for CLI/working dir selection + const result = await startupSetup(); + cliName = result.cliName; + workingDir = result.workingDir; + } const hasDiscord = !!DISCORD_BOT_TOKEN; const hasLine = !!(LINE_CHANNEL_ACCESS_TOKEN && LINE_CHANNEL_SECRET); @@ -416,7 +445,15 @@ async function main(): Promise { console.warn(); } - const { cliName, workingDir } = await startupSetup(); + // Ensure rules file exists + workingDir = path.resolve(workingDir || process.cwd()); + ensureRulesMd(cliName, workingDir); + + console.log(); + console.log("=".repeat(48)); + console.log(` ${CLI_TOOLS[cliName].name} @ ${workingDir}`); + console.log("=".repeat(48)); + console.log(); // Initialize audit logger if (AUDIT_LOG_ENABLED) { diff --git a/src/setup/setupPage.html b/src/setup/setupPage.html new file mode 100644 index 0000000..631be96 --- /dev/null +++ b/src/setup/setupPage.html @@ -0,0 +1,637 @@ + + + + + + AI CLI Gateway Bot - Setup + + + +
+
+

AI CLI Gateway Bot

+

Discord / LINE에서 AI CLI 도구를 원격 제어하세요

+
+ +
+ +
+
+
+
+
플랫폼 선택
+
사용할 메신저를 선택하세요
+
+
+ +
+
+ 💬 + Discord +
+
+
+ +
+
+ 💬 + LINE +
+
+
+
+ + +
+
+
💬
+
+
Discord 설정
+
Discord 봇 정보를 입력하세요
+
+
+ +
+
+ + +
Discord Developer Portal에서 복사
+
+ +
+ + +
17-19자리 숫자 (개발자 모드에서 복사)
+
+
+ + + ➔ Discord 봇 만들기 가이드 + +
+
+ + + + + +
+
+
🤖
+
+
AI CLI 도구
+
사용할 AI CLI를 선택하세요
+
+
+ +
+ +
+ +
+ + +
AI가 작업할 폴더 경로 (비워두면 현재 폴더)
+
+
+ + + +
+
+ + +
+ + + + diff --git a/src/setup/setupServer.ts b/src/setup/setupServer.ts new file mode 100644 index 0000000..224e9d7 --- /dev/null +++ b/src/setup/setupServer.ts @@ -0,0 +1,180 @@ +import http from "node:http"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawn } from "node:child_process"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +interface SetupData { + platforms: { discord: boolean; line: boolean }; + discord: { token: string; userId: string } | null; + line: { token: string; secret: string; userId: string; port: string } | null; + cli: string; + workingDir: string; +} + +interface SetupResult { + cliName: string; + workingDir: string; +} + +/** + * Starts a local web server for the setup page. + * Returns a promise that resolves when the user completes setup. + */ +export function startSetupServer(port = 5000): Promise { + return new Promise((resolve, reject) => { + let setupComplete = false; + + const server = http.createServer(async (req, res) => { + const url = new URL(req.url || "/", `http://localhost:${port}`); + + // Serve the setup page + if (url.pathname === "/" || url.pathname === "/index.html") { + const htmlPath = path.join(__dirname, "setupPage.html"); + try { + const html = fs.readFileSync(htmlPath, "utf-8"); + res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); + res.end(html); + } catch (err) { + res.writeHead(500, { "Content-Type": "text/plain" }); + res.end("Failed to load setup page"); + } + return; + } + + // Handle API endpoint + if (url.pathname === "/api/setup" && req.method === "POST") { + let body = ""; + req.on("data", (chunk) => (body += chunk)); + req.on("end", () => { + try { + const data: SetupData = JSON.parse(body); + const result = saveSetup(data); + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ success: true })); + + setupComplete = true; + + // Close server after response is sent + setTimeout(() => { + server.close(); + resolve(result); + }, 500); + } catch (err: any) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ success: false, error: err.message })); + } + }); + return; + } + + // 404 for other paths + res.writeHead(404, { "Content-Type": "text/plain" }); + res.end("Not Found"); + }); + + server.listen(port, () => { + console.log(); + console.log("=".repeat(48)); + console.log(" AI CLI Gateway Bot - Setup"); + console.log("=".repeat(48)); + console.log(); + console.log(` Setup page: http://localhost:${port}`); + console.log(); + console.log(" Opening browser..."); + console.log(); + + // Open browser + openBrowser(`http://localhost:${port}`); + }); + + server.on("error", (err) => { + reject(err); + }); + + // Timeout after 10 minutes + setTimeout(() => { + if (!setupComplete) { + server.close(); + reject(new Error("Setup timed out")); + } + }, 10 * 60 * 1000); + }); +} + +/** + * Save the setup data to .env file + */ +function saveSetup(data: SetupData): SetupResult { + const envPath = path.join(process.cwd(), ".env"); + const lines: string[] = []; + + // Discord settings + if (data.discord) { + lines.push("# Discord 설정"); + lines.push(`DISCORD_BOT_TOKEN=${data.discord.token}`); + lines.push(`ALLOWED_USER_IDS=${data.discord.userId}`); + lines.push(""); + } + + // LINE settings + if (data.line) { + lines.push("# LINE 설정"); + lines.push(`LINE_CHANNEL_ACCESS_TOKEN=${data.line.token}`); + lines.push(`LINE_CHANNEL_SECRET=${data.line.secret}`); + lines.push(`ALLOWED_LINE_USER_IDS=${data.line.userId}`); + lines.push(`LINE_WEBHOOK_PORT=${data.line.port}`); + lines.push(""); + } + + // Common settings + lines.push("# 공통 설정"); + lines.push("COMMAND_PREFIX=!"); + lines.push("COMMAND_TIMEOUT=30"); + lines.push("AI_CLI_TIMEOUT=300"); + + fs.writeFileSync(envPath, lines.join("\n") + "\n", "utf-8"); + + return { + cliName: data.cli || "claude", + workingDir: data.workingDir || process.cwd(), + }; +} + +/** + * Open the default browser with the given URL + */ +function openBrowser(url: string): void { + const platform = process.platform; + + let command: string; + let args: string[]; + + if (platform === "win32") { + command = "cmd"; + args = ["/c", "start", '""', url]; + } else if (platform === "darwin") { + command = "open"; + args = [url]; + } else { + command = "xdg-open"; + args = [url]; + } + + const child = spawn(command, args, { + detached: true, + stdio: "ignore", + shell: platform === "win32", + }); + child.unref(); +} + +/** + * Check if .env file exists + */ +export function envExists(): boolean { + return fs.existsSync(path.join(process.cwd(), ".env")); +} From aec610f56e5f834b7c01d5da55ec8527c759065b Mon Sep 17 00:00:00 2001 From: OSgoodYZ Date: Fri, 13 Feb 2026 11:01:40 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EC=9B=B9=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20=EB=B4=87=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=94=EB=A1=9C=EA=B0=80=EA=B8=B0=20=EB=B0=B0?= =?UTF-8?q?=EB=84=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Discord: "Discord 봇이 없으신가요?" 배너 클릭 시 Developer Portal로 이동 - LINE: "LINE 봇이 없으신가요?" 배너 클릭 시 LINE Official Account Manager로 이동 - 각 플랫폼 색상에 맞는 그라데이션 배너 디자인 - 호버 시 애니메이션 효과 추가 Co-Authored-By: Claude Opus 4.5 --- src/setup/setupPage.html | 93 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/src/setup/setupPage.html b/src/setup/setupPage.html index 631be96..7f96438 100644 --- a/src/setup/setupPage.html +++ b/src/setup/setupPage.html @@ -214,6 +214,71 @@ color: #FFA500; } + .create-bot-banner { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + border-radius: 12px; + margin-bottom: 20px; + cursor: pointer; + transition: all 0.3s ease; + } + + .create-bot-banner:hover { + transform: translateY(-2px); + } + + .discord-banner { + background: linear-gradient(135deg, rgba(88, 101, 242, 0.2), rgba(88, 101, 242, 0.1)); + border: 1px solid rgba(88, 101, 242, 0.4); + } + + .discord-banner:hover { + background: linear-gradient(135deg, rgba(88, 101, 242, 0.3), rgba(88, 101, 242, 0.15)); + box-shadow: 0 8px 25px rgba(88, 101, 242, 0.25); + } + + .line-banner { + background: linear-gradient(135deg, rgba(6, 199, 85, 0.2), rgba(6, 199, 85, 0.1)); + border: 1px solid rgba(6, 199, 85, 0.4); + } + + .line-banner:hover { + background: linear-gradient(135deg, rgba(6, 199, 85, 0.3), rgba(6, 199, 85, 0.15)); + box-shadow: 0 8px 25px rgba(6, 199, 85, 0.25); + } + + .banner-icon { + font-size: 2rem; + flex-shrink: 0; + } + + .banner-text { + flex: 1; + } + + .banner-title { + font-weight: 600; + font-size: 0.95rem; + margin-bottom: 2px; + } + + .banner-subtitle { + font-size: 0.8rem; + color: #a1a1aa; + } + + .banner-arrow { + font-size: 1.2rem; + color: #a1a1aa; + transition: transform 0.3s; + } + + .create-bot-banner:hover .banner-arrow { + transform: translateX(4px); + } + .platform-content { display: none; animation: fadeIn 0.3s ease; @@ -365,24 +430,30 @@

AI CLI Gateway Bot

+ +
+ + + +
+
-
Discord Developer Portal에서 복사
+
Discord Developer Portal > Bot > Reset Token
-
17-19자리 숫자 (개발자 모드에서 복사)
+
Discord 설정 > 고급 > 개발자 모드 ON > 프로필 우클릭 > ID 복사
- - - ➔ Discord 봇 만들기 가이드 -
@@ -397,6 +468,16 @@

AI CLI Gateway Bot

+ +
+ + + +
+
Date: Fri, 13 Feb 2026 11:12:43 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chore:=20package-lock.json=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.5 --- package-lock.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0ff96b2..8d2ebd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2360,8 +2360,7 @@ "version": "0.0.1566079", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz", "integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/diff": { "version": "8.0.3", @@ -3797,7 +3796,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4658,7 +4656,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -4698,7 +4695,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4745,7 +4741,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0",