discord-agent-bridge 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/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/app.js +210 -0
- package/dist/app.js.map +1 -0
- package/dist/app.test.js +117 -0
- package/dist/app.test.js.map +1 -0
- package/dist/cli.js +63 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.test.js +91 -0
- package/dist/cli.test.js.map +1 -0
- package/dist/core/auditLog.js +65 -0
- package/dist/core/auditLog.js.map +1 -0
- package/dist/core/auditLog.test.js +124 -0
- package/dist/core/auditLog.test.js.map +1 -0
- package/dist/core/auth.js +106 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/auth.test.js +216 -0
- package/dist/core/auth.test.js.map +1 -0
- package/dist/core/channelRegistry.js +119 -0
- package/dist/core/channelRegistry.js.map +1 -0
- package/dist/core/channelRegistry.test.js +114 -0
- package/dist/core/channelRegistry.test.js.map +1 -0
- package/dist/core/commandPolicy.js +6 -0
- package/dist/core/commandPolicy.js.map +1 -0
- package/dist/core/commandRouter.js +9 -0
- package/dist/core/commandRouter.js.map +1 -0
- package/dist/core/config.js +141 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/config.test.js +149 -0
- package/dist/core/config.test.js.map +1 -0
- package/dist/core/configResolver.js +90 -0
- package/dist/core/configResolver.js.map +1 -0
- package/dist/core/configResolver.test.js +138 -0
- package/dist/core/configResolver.test.js.map +1 -0
- package/dist/core/configSchema.js +164 -0
- package/dist/core/configSchema.js.map +1 -0
- package/dist/core/contracts.js +5 -0
- package/dist/core/contracts.js.map +1 -0
- package/dist/core/eventBus.js +42 -0
- package/dist/core/eventBus.js.map +1 -0
- package/dist/core/eventBus.test.js +72 -0
- package/dist/core/eventBus.test.js.map +1 -0
- package/dist/core/hookBridge.js +11 -0
- package/dist/core/hookBridge.js.map +1 -0
- package/dist/core/logger.js +116 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/logger.test.js +88 -0
- package/dist/core/logger.test.js.map +1 -0
- package/dist/core/modeRegistry.js +33 -0
- package/dist/core/modeRegistry.js.map +1 -0
- package/dist/core/permissionResolver.js +43 -0
- package/dist/core/permissionResolver.js.map +1 -0
- package/dist/core/permissionResolver.test.js +136 -0
- package/dist/core/permissionResolver.test.js.map +1 -0
- package/dist/core/providerCatalog.js +240 -0
- package/dist/core/providerCatalog.js.map +1 -0
- package/dist/core/providerCatalog.test.js +230 -0
- package/dist/core/providerCatalog.test.js.map +1 -0
- package/dist/core/sessionOrchestrator.js +463 -0
- package/dist/core/sessionOrchestrator.js.map +1 -0
- package/dist/core/sessionOrchestrator.test.js +344 -0
- package/dist/core/sessionOrchestrator.test.js.map +1 -0
- package/dist/core/state/schema.js +49 -0
- package/dist/core/state/schema.js.map +1 -0
- package/dist/core/state/store.js +86 -0
- package/dist/core/state/store.js.map +1 -0
- package/dist/core/state/store.test.js +99 -0
- package/dist/core/state/store.test.js.map +1 -0
- package/dist/core/usageService.js +310 -0
- package/dist/core/usageService.js.map +1 -0
- package/dist/core/usageService.test.js +234 -0
- package/dist/core/usageService.test.js.map +1 -0
- package/dist/discord/alwaysAllow.test.js +181 -0
- package/dist/discord/alwaysAllow.test.js.map +1 -0
- package/dist/discord/client.js +666 -0
- package/dist/discord/client.js.map +1 -0
- package/dist/discord/client.test.js +256 -0
- package/dist/discord/client.test.js.map +1 -0
- package/dist/discord/configPanel.js +270 -0
- package/dist/discord/configPanel.js.map +1 -0
- package/dist/discord/configPanel.test.js +332 -0
- package/dist/discord/configPanel.test.js.map +1 -0
- package/dist/discord/directoryBrowser.js +151 -0
- package/dist/discord/directoryBrowser.js.map +1 -0
- package/dist/discord/directoryBrowser.test.js +163 -0
- package/dist/discord/directoryBrowser.test.js.map +1 -0
- package/dist/discord/favorites.js +10 -0
- package/dist/discord/favorites.js.map +1 -0
- package/dist/discord/fileDownload.js +94 -0
- package/dist/discord/fileDownload.js.map +1 -0
- package/dist/discord/fileDownload.test.js +59 -0
- package/dist/discord/fileDownload.test.js.map +1 -0
- package/dist/discord/format.js +104 -0
- package/dist/discord/format.js.map +1 -0
- package/dist/discord/format.test.js +40 -0
- package/dist/discord/format.test.js.map +1 -0
- package/dist/discord/guildChannels.js +97 -0
- package/dist/discord/guildChannels.js.map +1 -0
- package/dist/discord/guildChannels.test.js +231 -0
- package/dist/discord/guildChannels.test.js.map +1 -0
- package/dist/discord/i18n.js +207 -0
- package/dist/discord/i18n.js.map +1 -0
- package/dist/discord/i18n.test.js +31 -0
- package/dist/discord/i18n.test.js.map +1 -0
- package/dist/discord/interactionRouter.js +954 -0
- package/dist/discord/interactionRouter.js.map +1 -0
- package/dist/discord/interactionRouter.test.js +1245 -0
- package/dist/discord/interactionRouter.test.js.map +1 -0
- package/dist/discord/messageRouter.js +221 -0
- package/dist/discord/messageRouter.js.map +1 -0
- package/dist/discord/messageRouter.test.js +360 -0
- package/dist/discord/messageRouter.test.js.map +1 -0
- package/dist/discord/ports.js +12 -0
- package/dist/discord/ports.js.map +1 -0
- package/dist/discord/renderers/defaultSet.test.js +97 -0
- package/dist/discord/renderers/defaultSet.test.js.map +1 -0
- package/dist/discord/renderers/diffView.js +73 -0
- package/dist/discord/renderers/diffView.js.map +1 -0
- package/dist/discord/renderers/diffView.test.js +54 -0
- package/dist/discord/renderers/diffView.test.js.map +1 -0
- package/dist/discord/renderers/index.js +143 -0
- package/dist/discord/renderers/index.js.map +1 -0
- package/dist/discord/renderers/index.test.js +124 -0
- package/dist/discord/renderers/index.test.js.map +1 -0
- package/dist/discord/renderers/mentionOnComplete.js +19 -0
- package/dist/discord/renderers/mentionOnComplete.js.map +1 -0
- package/dist/discord/renderers/mentionOnComplete.test.js +35 -0
- package/dist/discord/renderers/mentionOnComplete.test.js.map +1 -0
- package/dist/discord/renderers/permissionButtons.js +150 -0
- package/dist/discord/renderers/permissionButtons.js.map +1 -0
- package/dist/discord/renderers/permissionButtons.test.js +108 -0
- package/dist/discord/renderers/permissionButtons.test.js.map +1 -0
- package/dist/discord/renderers/resultLine.js +25 -0
- package/dist/discord/renderers/resultLine.js.map +1 -0
- package/dist/discord/renderers/statusEmbed.js +20 -0
- package/dist/discord/renderers/statusEmbed.js.map +1 -0
- package/dist/discord/renderers/statusEmbed.test.js +32 -0
- package/dist/discord/renderers/statusEmbed.test.js.map +1 -0
- package/dist/discord/renderers/streamEmbed.js +134 -0
- package/dist/discord/renderers/streamEmbed.js.map +1 -0
- package/dist/discord/renderers/streamEmbed.test.js +97 -0
- package/dist/discord/renderers/streamEmbed.test.js.map +1 -0
- package/dist/discord/renderers/toolThread.js +65 -0
- package/dist/discord/renderers/toolThread.js.map +1 -0
- package/dist/discord/renderers/toolThread.test.js +54 -0
- package/dist/discord/renderers/toolThread.test.js.map +1 -0
- package/dist/discord/renderers/transcriptFeed.js +33 -0
- package/dist/discord/renderers/transcriptFeed.js.map +1 -0
- package/dist/discord/renderers/usageEmbed.js +78 -0
- package/dist/discord/renderers/usageEmbed.js.map +1 -0
- package/dist/discord/renderers/usageEmbed.test.js +42 -0
- package/dist/discord/renderers/usageEmbed.test.js.map +1 -0
- package/dist/discord/wiring.js +214 -0
- package/dist/discord/wiring.js.map +1 -0
- package/dist/discord/wiring.test.js +252 -0
- package/dist/discord/wiring.test.js.map +1 -0
- package/dist/discord/wizard/channelWizard.js +305 -0
- package/dist/discord/wizard/channelWizard.js.map +1 -0
- package/dist/discord/wizard/channelWizard.test.js +298 -0
- package/dist/discord/wizard/channelWizard.test.js.map +1 -0
- package/dist/discord/wizard/resumeWizard.js +123 -0
- package/dist/discord/wizard/resumeWizard.js.map +1 -0
- package/dist/modes/claude/index.js +80 -0
- package/dist/modes/claude/index.js.map +1 -0
- package/dist/modes/claude/listResumable.test.js +72 -0
- package/dist/modes/claude/listResumable.test.js.map +1 -0
- package/dist/modes/claude/mcpFileTool.js +82 -0
- package/dist/modes/claude/mcpFileTool.js.map +1 -0
- package/dist/modes/claude/permissions.js +35 -0
- package/dist/modes/claude/permissions.js.map +1 -0
- package/dist/modes/claude/plugins.js +56 -0
- package/dist/modes/claude/plugins.js.map +1 -0
- package/dist/modes/claude/session.js +324 -0
- package/dist/modes/claude/session.js.map +1 -0
- package/dist/modes/claude/session.test.js +415 -0
- package/dist/modes/claude/session.test.js.map +1 -0
- package/dist/modes/codex/discovery.js +221 -0
- package/dist/modes/codex/discovery.js.map +1 -0
- package/dist/modes/codex/discovery.test.js +213 -0
- package/dist/modes/codex/discovery.test.js.map +1 -0
- package/dist/modes/codex/eventMapper.js +262 -0
- package/dist/modes/codex/eventMapper.js.map +1 -0
- package/dist/modes/codex/index.js +132 -0
- package/dist/modes/codex/index.js.map +1 -0
- package/dist/modes/codex/index.test.js +170 -0
- package/dist/modes/codex/index.test.js.map +1 -0
- package/dist/modes/codex/runner.js +207 -0
- package/dist/modes/codex/runner.js.map +1 -0
- package/dist/modes/codex/runner.test.js +587 -0
- package/dist/modes/codex/runner.test.js.map +1 -0
- package/dist/modes/codex/sqliteReader.js +171 -0
- package/dist/modes/codex/sqliteReader.js.map +1 -0
- package/dist/modes/codex/sqliteReader.test.js +137 -0
- package/dist/modes/codex/sqliteReader.test.js.map +1 -0
- package/dist/setup/wizard.js +115 -0
- package/dist/setup/wizard.js.map +1 -0
- package/dist/setup/wizard.test.js +216 -0
- package/dist/setup/wizard.test.js.map +1 -0
- package/dist/startBot.test.js +78 -0
- package/dist/startBot.test.js.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 10000DOO
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# discord-agent-bridge
|
|
2
|
+
|
|
3
|
+
> Self-hosted Discord bot that runs AI coding agents — Claude Code, Codex, and more — per channel. Role-based access, multi-server, extensible.
|
|
4
|
+
|
|
5
|
+
**채널마다 Claude Code / Codex 같은 AI 코딩 에이전트를 붙여 쓰는 셀프호스팅 Discord 봇입니다.** 내 PC에서 실행되고, 역할 기반 권한 · 다중 서버 · 확장 가능 구조를 가집니다.
|
|
6
|
+
|
|
7
|
+
- 현재 지원: **Claude Code**, **Codex**
|
|
8
|
+
- 확장: 모드 플러그인 추가로 다른 에이전트(예: opencode) 연결 가능
|
|
9
|
+
|
|
10
|
+
> 🚧 **상태: 개발 중 (Phase 1).** 아직 npm에 배포되지 않았습니다 — 지금은 아래 **"소스에서 실행"** 방법으로만 동작합니다. `npx` 배포는 완성 후 제공됩니다. 이 가이드는 완성 시점의 사용 흐름 기준입니다.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 이게 뭐예요?
|
|
15
|
+
|
|
16
|
+
Discord 채널에서 대화하듯 메시지를 보내면, 내 컴퓨터에서 Claude Code(또는 Codex)가 그 프로젝트 폴더를 대상으로 작업해 주는 봇입니다.
|
|
17
|
+
|
|
18
|
+
- **채널 = 세션 = 프로젝트 하나.** 채널을 만들 때 작업 폴더와 백엔드(Claude/Codex)를 정합니다.
|
|
19
|
+
- **여러 서버**에 같은 봇을 초대해 쓸 수 있고, **서버/프로젝트마다 설정이 따로** 적용됩니다.
|
|
20
|
+
- **역할(Role)로 누가 쓸 수 있는지** 통제합니다.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 준비물
|
|
25
|
+
|
|
26
|
+
- **Node.js 20 이상**
|
|
27
|
+
- 사용할 백엔드에 맞는 CLI가 **설치 + 로그인**되어 있어야 합니다:
|
|
28
|
+
- **Claude 모드** → [Claude Code](https://docs.anthropic.com/en/docs/claude-code) 인증 (`claude` 로그인 또는 `ANTHROPIC_API_KEY`). 사용량/한도 패널을 보려면 **Claude Pro/Max 구독 로그인**이 필요합니다.
|
|
29
|
+
- **Codex 모드** → `codex` CLI 설치 + 로그인.
|
|
30
|
+
- **Discord 봇 토큰** (아래 1단계에서 발급)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 1단계 — Discord 봇 만들기 (Developer Portal)
|
|
35
|
+
|
|
36
|
+
봇을 직접 하나 만들어야 합니다. 5분이면 됩니다.
|
|
37
|
+
|
|
38
|
+
1. **[Discord Developer Portal](https://discord.com/developers/applications)** 접속 → 오른쪽 위 **New Application** → 이름 입력(예: `my-agent-bot`) → **Create**.
|
|
39
|
+
2. 왼쪽 메뉴 **Bot** 탭 → **Reset Token** → 나오는 **토큰을 복사**해 안전한 곳에 보관.
|
|
40
|
+
- ⚠️ 이 토큰은 비밀번호입니다. 노출되면 즉시 **Reset Token**으로 재발급하세요.
|
|
41
|
+
3. 같은 **Bot** 탭 아래 **Privileged Gateway Intents**:
|
|
42
|
+
- ✅ **MESSAGE CONTENT INTENT** — **필수** (봇이 메시지 내용을 읽어야 함)
|
|
43
|
+
- ✅ **SERVER MEMBERS INTENT** — 권장 (역할 기반 권한 확인에 사용)
|
|
44
|
+
- 켜고 **Save Changes**.
|
|
45
|
+
4. 왼쪽 **OAuth2** 탭 → **Client ID(Application ID)** 복사 (셋업에 필요).
|
|
46
|
+
5. **초대 링크 만들기** — OAuth2 → **URL Generator**:
|
|
47
|
+
- **Scopes**: `bot`, `applications.commands`
|
|
48
|
+
- **Bot Permissions**: `Manage Channels`, `Send Messages`, `Embed Links`, `Attach Files`, `Read Message History`, `Create Public Threads`, `Send Messages in Threads`, `Manage Threads`, `Add Reactions`
|
|
49
|
+
- 생성된 URL을 브라우저에 붙여넣어 **내 서버에 초대**.
|
|
50
|
+
- (셋업 마법사가 이 초대 링크를 자동 생성해 주기도 합니다 — 2단계 참고.)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 2단계 — 설치 & 실행
|
|
55
|
+
|
|
56
|
+
### (완성 후) npx로 실행
|
|
57
|
+
```bash
|
|
58
|
+
# 최초 실행 — 그냥 실행하면 셋업 마법사가 자동으로 뜬 뒤 이어서 봇이 시작됩니다.
|
|
59
|
+
# (토큰/Client ID 입력, 인텐트 확인, 초대 링크 생성 — 기본값은 안 물어봄)
|
|
60
|
+
npx discord-agent-bridge@latest
|
|
61
|
+
|
|
62
|
+
# 이후 실행 — 이미 설정돼 있으면 바로 봇이 시작됩니다.
|
|
63
|
+
npx discord-agent-bridge@latest
|
|
64
|
+
|
|
65
|
+
# 다시 설정만 하고 싶을 때 (봇은 시작하지 않음)
|
|
66
|
+
npx discord-agent-bridge@latest --setup
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### (지금) 소스에서 실행
|
|
70
|
+
```bash
|
|
71
|
+
git clone https://github.com/10000DOO/discord-agent-bridge.git
|
|
72
|
+
cd discord-agent-bridge
|
|
73
|
+
npm install
|
|
74
|
+
npm run build
|
|
75
|
+
|
|
76
|
+
node dist/cli.js # 최초 1회: 셋업 마법사 자동 실행 → 이어서 봇 시작 / 이후: 바로 봇 시작
|
|
77
|
+
node dist/cli.js --setup # 설정만 다시 (봇은 시작 안 함)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**셋업 마법사(`--setup`)** 가 물어보는 것 — **토큰(비밀)만 터미널에서 입력**합니다. 그 외 값은 하나도 안 물어봅니다:
|
|
81
|
+
1. Discord 봇 **토큰** (비밀 → 터미널에서만 붙여넣기)
|
|
82
|
+
2. **Client ID**
|
|
83
|
+
3. Message Content Intent 켰는지 확인
|
|
84
|
+
4. 초대 URL 생성 → 서버에 초대
|
|
85
|
+
|
|
86
|
+
> **역할도, 기본값도 터미널에서 안 정합니다.** 봇을 서버에 초대한 뒤 Discord에서 **`/config`** 명령으로 설정하세요:
|
|
87
|
+
> - **역할 티어** — 역할 이름을 **클릭**해 지정(역할 ID 복사·개발자 모드 불필요). 지정 전에는 **모두 거부(deny-by-default)**.
|
|
88
|
+
> - **기본값** — **기본 백엔드·모델·권한 모드·언어(locale)** 는 드롭다운에서 고르면 **바로 저장**되고, **Codex 기본 경로(codexHome)** 는 "Codex 경로 설정" 버튼 → 입력창(모달)에서 지정합니다. 언어는 `한국어 (ko)` / `English (en)` 중에서 선택합니다.
|
|
89
|
+
|
|
90
|
+
설정은 `~/.discord-agent-bridge/` 에 저장됩니다(토큰 파일 권한 600). 역할·기본값은 서버별로 `servers/<guildId>.json` 에 저장됩니다.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 3단계 — Discord에서 사용하기
|
|
95
|
+
|
|
96
|
+
봇이 서버에 들어오고 실행 중이면 (권장 순서: **초대 → `/init` → `/config` → `/agent start`**):
|
|
97
|
+
|
|
98
|
+
1. **`/init`** (관리자) → 컨트롤 채널(`#session-generator`)과 세션 카테고리를 만듭니다. 다시 실행해도 기존 채널을 재사용합니다(중복 생성 없음).
|
|
99
|
+
2. **`/config`** (관리자) → 역할 티어·기본값을 지정합니다. (서버 관리자(Administrator)는 역할 설정 없이도 항상 사용할 수 있습니다.)
|
|
100
|
+
3. `#session-generator` 에서 **`/agent start`** → **마법사**가 순서대로 안내합니다:
|
|
101
|
+
**작업 폴더 선택 → 백엔드(Claude / Codex) → 모델 → 권한 모드/프로필**. 확인하면 프로젝트 폴더 이름으로 **전용 세션 채널(`proj-<폴더>`)이 새로 생성**되고 그 채널에 바인딩됩니다.
|
|
102
|
+
4. 만들어진 세션 채널에서 **그냥 메시지로 대화**하면 됩니다. Claude 모드는 스트리밍 출력·툴 실행 스레드·권한 승인 버튼이 뜹니다.
|
|
103
|
+
5. 필요할 때 명령어 사용.
|
|
104
|
+
|
|
105
|
+
### 주요 명령어
|
|
106
|
+
| 명령어 | 설명 |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `/init` | (관리자) 컨트롤 채널 + 세션 카테고리 생성 (재실행 시 재사용) |
|
|
109
|
+
| `/agent start` | 새 세션 시작 — 마법사 확인 시 전용 세션 채널을 새로 생성 |
|
|
110
|
+
| `/agent resume` | 이전 세션 이어하기 |
|
|
111
|
+
| `/agent close` | 세션 종료 + 세션 채널 삭제 |
|
|
112
|
+
| `/mode <claude\|codex>` | 백엔드 전환 (⚠️ 새 대화로 시작 — 이전 맥락 미승계) |
|
|
113
|
+
| `/mode perm <모드\|프로필>` | 권한 모드/프로필 전환 (세션 유지) |
|
|
114
|
+
| `/stop` | 현재 세션 즉시 중단 (킬 스위치) |
|
|
115
|
+
| `/stop-all` | (관리자) 모든 세션 중단 |
|
|
116
|
+
| `/config` | (관리자) 역할 티어 + 기본값(백엔드·모델·권한 모드·언어·Codex 경로) 설정 — 역할은 **클릭**, 기본값은 드롭다운/모달에서 지정 |
|
|
117
|
+
|
|
118
|
+
### 권한 모드 (세션이 얼마나 자율적인지)
|
|
119
|
+
- `default` — 도구 실행 전마다 **Allow/Deny 버튼**으로 확인 (가장 안전)
|
|
120
|
+
- `acceptEdits` — 파일 수정은 자동 수락
|
|
121
|
+
- `plan` — 먼저 계획만 세우고 실행은 보류
|
|
122
|
+
- `bypassPermissions` — 전부 자동 (신뢰하는 프로젝트에서만)
|
|
123
|
+
|
|
124
|
+
(Codex는 CLI의 승인·샌드박스 모드로 매핑됩니다.)
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 권한 & 역할 설정 (중요)
|
|
129
|
+
|
|
130
|
+
이 봇은 **당신 PC에서, 당신 계정 권한으로** 코드를 실행합니다. 즉 봇에게 명령할 수 있는 사람은 **당신 컴퓨터에서 명령을 실행할 수 있는 사람**입니다. 그래서 역할 통제가 필수입니다.
|
|
131
|
+
|
|
132
|
+
- **역할 티어 3단계** — Discord에서 **`/config`** 로 역할 이름을 **클릭**해 지정합니다(역할 ID·개발자 모드 불필요). admin ⊇ execute ⊇ read-only:
|
|
133
|
+
- **admin** — 설정/`stop-all`/`config` 등 관리
|
|
134
|
+
- **execute** — 세션 시작·명령 실행
|
|
135
|
+
- **read-only** — 읽기만
|
|
136
|
+
- > `/config` 는 처음엔 **서버 관리자(Administrator)** 권한이 있는 사람만 열 수 있고(허용 목록이 비어 있어도 부트스트랩 가능), 이후엔 admin 티어도 사용할 수 있습니다.
|
|
137
|
+
- **기본은 거부(deny-by-default)** — 허용 목록에 없는 사람은 아무것도 못 합니다. `/config` 로 지정하기 전에는 아무도 실행할 수 없습니다.
|
|
138
|
+
- **프로젝트별 접근 제어(ACL)** — 특정 프로젝트를 지정한 역할/사람만 쓰게 할 수 있습니다.
|
|
139
|
+
- **감사 로그** — 누가 언제 무엇을 했는지 `~/.discord-agent-bridge/audit/` 에 기록됩니다.
|
|
140
|
+
|
|
141
|
+
> ⚠️ **보안 주의:** 신뢰하지 않는 서버에 봇을 초대하거나 execute 역할을 너무 넓게 주지 마세요. 작업 폴더 밖(예: `~/.ssh`)은 봇이 접근하지 못하도록 기본 차단되어 있습니다.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Claude 모드 vs Codex 모드
|
|
146
|
+
|
|
147
|
+
| | Claude 모드 | Codex 모드 |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| 실시간 스트리밍 | ✅ | ❌ (최종 결과 위주) |
|
|
150
|
+
| 툴 실행 스레드 | ✅ | ❌ |
|
|
151
|
+
| 권한 승인 버튼 | ✅ | ❌ (승인/샌드박스 모드로 대체) |
|
|
152
|
+
| 세션 이어하기 | ✅ | ✅ |
|
|
153
|
+
| **사용량/한도 표시** | ✅ (5시간·주간·컨텍스트) | ❌ (Codex CLI 제약) |
|
|
154
|
+
|
|
155
|
+
### 터미널에서 쓰던 거랑 똑같나요?
|
|
156
|
+
|
|
157
|
+
- **Claude 모드** — 터미널의 `claude`와 **같은 엔진**(공식 Claude Agent SDK)을 사용합니다. 프로젝트의 `.claude/` 설정을 그대로 읽어 **서브에이전트·스킬·훅·MCP가 동일하게** 동작하도록 만듭니다. 달라지는 건 입력이 Discord 메시지, 출력이 임베드/스레드라는 **표현 방식**뿐입니다. (터미널에서 타이핑하는 TUI 전용 슬래시 명령은 그대로 치는 방식이 아니라 SDK 방식으로 처리됩니다.)
|
|
158
|
+
- **Codex 모드** — `codex exec`(비대화형)로 구동합니다. 같은 Codex 엔진에 설정/MCP를 로드하지만, **비대화형 모드가 인터랙티브 모드와 일부 다를 수 있어**(승인 방식·특정 기능 등) **서브에이전트·스킬·MCP가 그대로 되는지는 Phase 2에서 실제 `codex` CLI로 검증**한 뒤 확정합니다. 검증 전까지는 완전 동일하다고 보장하지 않습니다.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 설정 파일 위치
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
~/.discord-agent-bridge/
|
|
166
|
+
├─ config.json # 봇 토큰·Client ID·기본값·한도 (권한 600)
|
|
167
|
+
├─ servers/<guildId>.json # 서버별 역할 티어·기본값 + /init 채널 구조(control/sessions) ID (권한 600)
|
|
168
|
+
├─ state.json # 채널↔세션 바인딩 (재시작 후 자동 복구)
|
|
169
|
+
└─ audit/audit.jsonl # 감사 로그
|
|
170
|
+
```
|
|
171
|
+
설정은 **전역 → 서버 → 프로젝트** 순으로 덮어써집니다.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 문제 해결
|
|
176
|
+
|
|
177
|
+
- **봇이 메시지에 반응하지 않아요** → Developer Portal에서 **Message Content Intent**가 켜져 있는지 확인.
|
|
178
|
+
- **슬래시 명령이 안 보여요** → 초대 시 `applications.commands` 스코프를 포함했는지 확인(등록에 몇 분 걸릴 수 있음).
|
|
179
|
+
- **권한 오류로 채널 생성 실패** → 봇에게 `Manage Channels` 권한이 있는지 확인.
|
|
180
|
+
- **사용량 패널이 안 떠요** → Claude Pro/Max **구독 로그인**(`~/.claude`) 상태여야 합니다. API 키만 쓰면 이 패널은 숨겨집니다(정상).
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 개발
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
npm install
|
|
188
|
+
npm run dev # tsx watch (개발 모드)
|
|
189
|
+
npm run typecheck # 타입 검사
|
|
190
|
+
npm run test # 테스트 (vitest)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## 라이선스
|
|
194
|
+
|
|
195
|
+
MIT
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { PermissionFlagsBits } from 'discord.js';
|
|
2
|
+
import { ConfigStore } from './core/config.js';
|
|
3
|
+
import { StateStore } from './core/state/store.js';
|
|
4
|
+
import { ChannelRegistry } from './core/channelRegistry.js';
|
|
5
|
+
import { EventBus } from './core/eventBus.js';
|
|
6
|
+
import { ConfigResolver } from './core/configResolver.js';
|
|
7
|
+
import { PermissionResolver } from './core/permissionResolver.js';
|
|
8
|
+
import { Authorizer } from './core/auth.js';
|
|
9
|
+
import { AuditLog } from './core/auditLog.js';
|
|
10
|
+
import { UsageService } from './core/usageService.js';
|
|
11
|
+
import { ModeRegistry } from './core/modeRegistry.js';
|
|
12
|
+
import { SessionOrchestrator } from './core/sessionOrchestrator.js';
|
|
13
|
+
import { createLogger } from './core/logger.js';
|
|
14
|
+
import { ClaudeMode } from './modes/claude/index.js';
|
|
15
|
+
import { CodexMode } from './modes/codex/index.js';
|
|
16
|
+
import { getClaudeModels, getClaudeModelsCachedOrFallback, getCodexModels } from './core/providerCatalog.js';
|
|
17
|
+
import { MessageRouter } from './discord/messageRouter.js';
|
|
18
|
+
import { InteractionRouter } from './discord/interactionRouter.js';
|
|
19
|
+
import { SessionWiring } from './discord/wiring.js';
|
|
20
|
+
import { DiscordClient, resolveGuildProvisioner } from './discord/client.js';
|
|
21
|
+
import { autoProvisionGuild } from './discord/guildChannels.js';
|
|
22
|
+
import { setLocale, t } from './discord/i18n.js';
|
|
23
|
+
export function createApp(deps) {
|
|
24
|
+
const { config, configStore } = deps;
|
|
25
|
+
// Locale is a global default the i18n catalog reads; set it before any user-facing
|
|
26
|
+
// string is rendered (§8.1 config.locale, §11 item 14).
|
|
27
|
+
setLocale(config.locale ?? 'ko');
|
|
28
|
+
const logger = deps.logger ?? createLogger('app', { level: config.logLevel });
|
|
29
|
+
// ---- Core graph (§2). Dependencies flow one way: everything below depends on
|
|
30
|
+
// ConfigStore/StateStore, nothing in core depends on Discord or a mode. ----
|
|
31
|
+
const stateStore = new StateStore(configStore.dir);
|
|
32
|
+
const channelRegistry = new ChannelRegistry(stateStore);
|
|
33
|
+
const eventBus = new EventBus();
|
|
34
|
+
const configResolver = new ConfigResolver(configStore, channelRegistry);
|
|
35
|
+
const permissionResolver = new PermissionResolver(configStore, configResolver);
|
|
36
|
+
const authorizer = new Authorizer(configStore, channelRegistry);
|
|
37
|
+
const auditLog = new AuditLog({ baseDir: configStore.dir });
|
|
38
|
+
const usageService = new UsageService({
|
|
39
|
+
logger,
|
|
40
|
+
userAgentVersion: config.usage.userAgent,
|
|
41
|
+
cacheSec: config.usage.cacheSec,
|
|
42
|
+
...(deps.fetchFn ? { fetchFn: deps.fetchFn } : {}),
|
|
43
|
+
});
|
|
44
|
+
const modeRegistry = new ModeRegistry();
|
|
45
|
+
// ---- Discord session wiring (renderers + permission buttons + sendFile) ----
|
|
46
|
+
// Built BEFORE the orchestrator so its requestPermission hook can be injected into
|
|
47
|
+
// the orchestrator at construction (the orchestrator's requestPermission is a
|
|
48
|
+
// constructor dep, not a mutable field). resolveChannel is bound to the live
|
|
49
|
+
// gateway once the client exists (below), via setResolveChannel.
|
|
50
|
+
const wiring = new SessionWiring({
|
|
51
|
+
eventBus,
|
|
52
|
+
modeRegistry,
|
|
53
|
+
channelRegistry,
|
|
54
|
+
usageService,
|
|
55
|
+
logger,
|
|
56
|
+
auditLog,
|
|
57
|
+
permissionTimeoutSec: config.limits.permissionTimeoutSec,
|
|
58
|
+
// Always-allow persistence (§7A): a tool the operator chose "always-allow" for
|
|
59
|
+
// is written into the GLOBAL autoAllowClaudeTools set, so the next turn (on any
|
|
60
|
+
// channel) auto-allows it via PermissionResolver → orchestrator → canUseTool.
|
|
61
|
+
// The wiring layer audits the GLOBAL write (who/where) before this runs.
|
|
62
|
+
onAlwaysAllow: (toolName) => {
|
|
63
|
+
configStore.addAutoAllowClaudeTool(toolName);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const orchestrator = new SessionOrchestrator({
|
|
67
|
+
channelRegistry,
|
|
68
|
+
modeRegistry,
|
|
69
|
+
eventBus,
|
|
70
|
+
configResolver,
|
|
71
|
+
permissionResolver,
|
|
72
|
+
auditLog,
|
|
73
|
+
logger,
|
|
74
|
+
// Route orchestrator permission prompts through the wiring's per-channel
|
|
75
|
+
// PermissionButtonsHandler (§7.5); the config timeout denies on expiry.
|
|
76
|
+
requestPermission: wiring.requestPermission,
|
|
77
|
+
});
|
|
78
|
+
// ---- Register the backends (§4/§10). Registering a mode automatically surfaces
|
|
79
|
+
// it as a `/mode backend` choice and in the channel wizard (both read
|
|
80
|
+
// modeRegistry.list()). ----
|
|
81
|
+
// Claude: sendFileFor is wired to the wiring layer's per-channel sink factory so
|
|
82
|
+
// the in-process attach_file MCP tool can deliver a confined file to the channel a
|
|
83
|
+
// session is bound to (kept out of the mode so modes stay transport-agnostic).
|
|
84
|
+
modeRegistry.register(new ClaudeMode({ sendFileFor: (guildId, channelId) => wiring.sendFileFor(guildId, channelId) }));
|
|
85
|
+
// Codex: no transport-specific deps (fileAttach:false → no sendFile); the runner
|
|
86
|
+
// and ~/.codex discovery are wired inside the mode. Its capabilities disable the
|
|
87
|
+
// Discord renderers Codex doesn't support (§5b/§6).
|
|
88
|
+
modeRegistry.register(new CodexMode());
|
|
89
|
+
// ---- Discord client (§2/§4). onReady resumes persisted sessions AND re-attaches a
|
|
90
|
+
// RendererDispatcher per resumed channel so a restart restores the live UX (§9). ----
|
|
91
|
+
const messageRouter = new MessageRouter({
|
|
92
|
+
authorizer,
|
|
93
|
+
channelRegistry,
|
|
94
|
+
orchestrator,
|
|
95
|
+
// The router arms a one-shot listener on this bus per accepted turn to clear the
|
|
96
|
+
// ⏳ working indicator (→ ✅/❌) when the channel's turn finishes.
|
|
97
|
+
eventBus,
|
|
98
|
+
logger,
|
|
99
|
+
// A server admin can drive a session by messaging even with an empty role config
|
|
100
|
+
// (never locked out): the router reads this bit off the member's permissions.
|
|
101
|
+
administratorBit: PermissionFlagsBits.Administrator,
|
|
102
|
+
});
|
|
103
|
+
const interactionRouter = new InteractionRouter({
|
|
104
|
+
authorizer,
|
|
105
|
+
orchestrator,
|
|
106
|
+
channelRegistry,
|
|
107
|
+
configStore,
|
|
108
|
+
configResolver,
|
|
109
|
+
permissionResolver,
|
|
110
|
+
modeRegistry,
|
|
111
|
+
wiring,
|
|
112
|
+
logger,
|
|
113
|
+
// Folder-browser roots + per-backend model list are config-driven (§8.1): the
|
|
114
|
+
// saved project favorites seed the browse roots; the model step offers the
|
|
115
|
+
// per-backend model list. Codex's list is a small documented default (the
|
|
116
|
+
// config `defaults.codexModel` — when set — plus common Codex model ids), so the
|
|
117
|
+
// wizard's model step isn't empty; a configured value is offered first. Selecting
|
|
118
|
+
// a Codex model here is cosmetic (like Claude's): the effective Codex model comes
|
|
119
|
+
// from config.defaults.codexModel — empty means `codex` uses its own config.toml
|
|
120
|
+
// default (operator-set). Override defaults.codexModel to force a model.
|
|
121
|
+
browseRoots: config.favorites,
|
|
122
|
+
// Per-backend model options from the central provider catalog (§ providerCatalog).
|
|
123
|
+
// Codex: a documented static default list (Codex has no model-list API; -m is
|
|
124
|
+
// free-form), with config.defaults.codexModel offered first when set. Claude:
|
|
125
|
+
// the SDK's supportedModels(), fetched once after login and CACHED — this render
|
|
126
|
+
// returns the cached English list if present, else the alias fallback while the
|
|
127
|
+
// async fetch warms the cache for the next render (never blocks the ack).
|
|
128
|
+
modelsFor: (backend) => backend === 'codex'
|
|
129
|
+
? getCodexModels(config.defaults.codexModel)
|
|
130
|
+
: getClaudeModelsCachedOrFallback({ logger }),
|
|
131
|
+
});
|
|
132
|
+
const discord = new DiscordClient({
|
|
133
|
+
clientId: config.discord.clientId,
|
|
134
|
+
logger,
|
|
135
|
+
messageRouter,
|
|
136
|
+
interactionRouter,
|
|
137
|
+
// Only registered backends are offered as `/mode backend` choices. Evaluated at
|
|
138
|
+
// command-registration time, so both Claude and Codex now appear.
|
|
139
|
+
backends: () => modeRegistry.list(),
|
|
140
|
+
onReady: async (client) => {
|
|
141
|
+
// Resume every persisted, non-archived channel binding (fixes A2), then
|
|
142
|
+
// re-attach its renderers so the live UX survives a restart (§9 step 4).
|
|
143
|
+
await orchestrator.resumeAll();
|
|
144
|
+
for (const binding of channelRegistry.list().filter((b) => !b.archived)) {
|
|
145
|
+
await wiring.attach(binding.guildId, binding.channelId, binding.mode);
|
|
146
|
+
}
|
|
147
|
+
// Warm the Claude model cache now that auth is available (fire-and-forget): the
|
|
148
|
+
// SDK's supportedModels() is fetched once and cached so the first /config or
|
|
149
|
+
// /agent start shows the real, current model list instead of the alias fallback.
|
|
150
|
+
void getClaudeModels({ logger });
|
|
151
|
+
logger.info('boot complete', { guilds: client.guilds.cache.size });
|
|
152
|
+
},
|
|
153
|
+
// Auto-provision each guild's channel structure on ready / guild-join so /init is
|
|
154
|
+
// optional. Resolves the guild's provisioner over the live gateway, then runs the
|
|
155
|
+
// idempotent, Manage-Channels-guarded, non-throwing provisioner.
|
|
156
|
+
autoProvisionGuild: async (guildId) => {
|
|
157
|
+
const provisioner = await resolveGuildProvisioner(discord.raw, guildId);
|
|
158
|
+
if (provisioner)
|
|
159
|
+
await autoProvisionGuild(provisioner, configStore, logger);
|
|
160
|
+
},
|
|
161
|
+
...(deps.client ? { client: deps.client } : {}),
|
|
162
|
+
});
|
|
163
|
+
// Bind the wiring's channel resolver to the live gateway now that the client exists.
|
|
164
|
+
wiring.setResolveChannel(SessionWiring.resolveOverClient(discord.raw));
|
|
165
|
+
// Bind the interaction router's guild/channel resolvers for /init + auto-created
|
|
166
|
+
// session channels (same late-binding pattern: the client depends on the router).
|
|
167
|
+
interactionRouter.setResolveGuildProvisioner((guildId) => resolveGuildProvisioner(discord.raw, guildId));
|
|
168
|
+
interactionRouter.setResolveChannel(SessionWiring.resolveOverClient(discord.raw));
|
|
169
|
+
return {
|
|
170
|
+
discord,
|
|
171
|
+
orchestrator,
|
|
172
|
+
modeRegistry,
|
|
173
|
+
wiring,
|
|
174
|
+
usageService,
|
|
175
|
+
logger,
|
|
176
|
+
config,
|
|
177
|
+
login: () => discord.login(config.discord.token),
|
|
178
|
+
destroy: () => discord.destroy(),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// Boot the bot. The two EXPECTED first-run conditions — no config file yet, or a
|
|
182
|
+
// config with no Discord token — are not bugs: they mean the operator hasn't run
|
|
183
|
+
// `--setup`. For those we print a short, actionable message and set a non-zero exit
|
|
184
|
+
// code, then return undefined (no App), instead of letting a raw stack trace out.
|
|
185
|
+
// Any OTHER failure (a corrupt/invalid config, a genuine login error) still throws
|
|
186
|
+
// so real bugs surface. Returns the running App, or undefined when we bailed with a
|
|
187
|
+
// friendly message.
|
|
188
|
+
export async function startBot(opts = {}) {
|
|
189
|
+
const configStore = new ConfigStore(opts.baseDir);
|
|
190
|
+
// Not set up yet: no config.json. Point the user at the wizard, exit cleanly.
|
|
191
|
+
if (!configStore.exists()) {
|
|
192
|
+
console.error(t('boot.noConfig'));
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
// A present-but-invalid config is a real error and propagates (not the friendly path).
|
|
197
|
+
const config = configStore.load();
|
|
198
|
+
// Config exists but the token is missing/empty: discord.login('') would dump a
|
|
199
|
+
// stack. Give the same friendly, actionable guidance instead.
|
|
200
|
+
if (config.discord.token.trim().length === 0) {
|
|
201
|
+
setLocale(config.locale ?? 'ko');
|
|
202
|
+
console.error(t('boot.noToken'));
|
|
203
|
+
process.exitCode = 1;
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
const app = createApp({ config, configStore });
|
|
207
|
+
await app.login();
|
|
208
|
+
return app;
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=app.js.map
|
package/dist/app.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAe,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAkB,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,+BAA+B,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC7G,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,CAAC,EAAe,MAAM,mBAAmB,CAAC;AA8C9D,MAAM,UAAU,SAAS,CAAC,IAAmB;IAC3C,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IAErC,mFAAmF;IACnF,wDAAwD;IACxD,SAAS,CAAE,MAAM,CAAC,MAAiB,IAAI,IAAI,CAAC,CAAC;IAE7C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,YAAY,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IAE9E,+EAA+E;IAC/E,6EAA6E;IAC7E,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACnD,MAAM,eAAe,GAAG,IAAI,eAAe,CAAC,UAAU,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;IAChC,MAAM,cAAc,GAAG,IAAI,cAAc,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC;IACxE,MAAM,kBAAkB,GAAG,IAAI,kBAAkB,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IAC/E,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC;IAChE,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,EAAE,OAAO,EAAE,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC;IAC5D,MAAM,YAAY,GAAG,IAAI,YAAY,CAAC;QACpC,MAAM;QACN,gBAAgB,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS;QACxC,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ;QAC/B,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACnD,CAAC,CAAC;IACH,MAAM,YAAY,GAAG,IAAI,YAAY,EAAE,CAAC;IAExC,+EAA+E;IAC/E,mFAAmF;IACnF,8EAA8E;IAC9E,6EAA6E;IAC7E,iEAAiE;IACjE,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC;QAC/B,QAAQ;QACR,YAAY;QACZ,eAAe;QACf,YAAY;QACZ,MAAM;QACN,QAAQ;QACR,oBAAoB,EAAE,MAAM,CAAC,MAAM,CAAC,oBAAoB;QACxD,+EAA+E;QAC/E,gFAAgF;QAChF,8EAA8E;QAC9E,yEAAyE;QACzE,aAAa,EAAE,CAAC,QAAgB,EAAE,EAAE;YAClC,WAAW,CAAC,sBAAsB,CAAC,QAAQ,CAAC,CAAC;QAC/C,CAAC;KACF,CAAC,CAAC;IAEH,MAAM,YAAY,GAAG,IAAI,mBAAmB,CAAC;QAC3C,eAAe;QACf,YAAY;QACZ,QAAQ;QACR,cAAc;QACd,kBAAkB;QAClB,QAAQ;QACR,MAAM;QACN,yEAAyE;QACzE,wEAAwE;QACxE,iBAAiB,EAAE,MAAM,CAAC,iBAAiB;KAC5C,CAAC,CAAC;IAEH,iFAAiF;IACjF,sEAAsE;IACtE,6BAA6B;IAC7B,iFAAiF;IACjF,mFAAmF;IACnF,+EAA+E;IAC/E,YAAY,CAAC,QAAQ,CACnB,IAAI,UAAU,CAAC,EAAE,WAAW,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC,CAChG,CAAC;IACF,iFAAiF;IACjF,iFAAiF;IACjF,oDAAoD;IACpD,YAAY,CAAC,QAAQ,CAAC,IAAI,SAAS,EAAE,CAAC,CAAC;IAEvC,oFAAoF;IACpF,sFAAsF;IACtF,MAAM,aAAa,GAAG,IAAI,aAAa,CAAC;QACtC,UAAU;QACV,eAAe;QACf,YAAY;QACZ,iFAAiF;QACjF,gEAAgE;QAChE,QAAQ;QACR,MAAM;QACN,iFAAiF;QACjF,8EAA8E;QAC9E,gBAAgB,EAAE,mBAAmB,CAAC,aAAa;KACpD,CAAC,CAAC;IACH,MAAM,iBAAiB,GAAG,IAAI,iBAAiB,CAAC;QAC9C,UAAU;QACV,YAAY;QACZ,eAAe;QACf,WAAW;QACX,cAAc;QACd,kBAAkB;QAClB,YAAY;QACZ,MAAM;QACN,MAAM;QACN,8EAA8E;QAC9E,2EAA2E;QAC3E,0EAA0E;QAC1E,iFAAiF;QACjF,kFAAkF;QAClF,kFAAkF;QAClF,iFAAiF;QACjF,yEAAyE;QACzE,WAAW,EAAE,MAAM,CAAC,SAAS;QAC7B,mFAAmF;QACnF,8EAA8E;QAC9E,8EAA8E;QAC9E,iFAAiF;QACjF,gFAAgF;QAChF,0EAA0E;QAC1E,SAAS,EAAE,CAAC,OAAe,EAAE,EAAE,CAC7B,OAAO,KAAK,OAAO;YACjB,CAAC,CAAC,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC;YAC5C,CAAC,CAAC,+BAA+B,CAAC,EAAE,MAAM,EAAE,CAAC;KAClD,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC;QAChC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,QAAQ;QACjC,MAAM;QACN,aAAa;QACb,iBAAiB;QACjB,gFAAgF;QAChF,kEAAkE;QAClE,QAAQ,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE;QACnC,OAAO,EAAE,KAAK,EAAE,MAAc,EAAE,EAAE;YAChC,wEAAwE;YACxE,yEAAyE;YACzE,MAAM,YAAY,CAAC,SAAS,EAAE,CAAC;YAC/B,KAAK,MAAM,OAAO,IAAI,eAAe,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACxE,MAAM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;YACxE,CAAC;YACD,gFAAgF;YAChF,6EAA6E;YAC7E,iFAAiF;YACjF,KAAK,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;YACjC,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QACrE,CAAC;QACD,kFAAkF;QAClF,kFAAkF;QAClF,iEAAiE;QACjE,kBAAkB,EAAE,KAAK,EAAE,OAAe,EAAE,EAAE;YAC5C,MAAM,WAAW,GAAG,MAAM,uBAAuB,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YACxE,IAAI,WAAW;gBAAE,MAAM,kBAAkB,CAAC,WAAW,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;QAC9E,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChD,CAAC,CAAC;IAEH,qFAAqF;IACrF,MAAM,CAAC,iBAAiB,CAAC,aAAa,CAAC,iBAAiB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IACvE,iFAAiF;IACjF,kFAAkF;IAClF,iBAAiB,CAAC,0BAA0B,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,uBAAuB,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;IACzG,iBAAiB,CAAC,iBAAiB,CAAC,aAAa,CAAC,iBAAiB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IAElF,OAAO;QACL,OAAO;QACP,YAAY;QACZ,YAAY;QACZ,MAAM;QACN,YAAY;QACZ,MAAM;QACN,MAAM;QACN,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;QAChD,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;KACjC,CAAC;AACJ,CAAC;AAQD,iFAAiF;AACjF,iFAAiF;AACjF,oFAAoF;AACpF,kFAAkF;AAClF,mFAAmF;AACnF,oFAAoF;AACpF,oBAAoB;AACpB,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,OAAwB,EAAE;IACvD,MAAM,WAAW,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAElD,8EAA8E;IAC9E,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,CAAC;QAC1B,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC;QAClC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACrB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,uFAAuF;IACvF,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,EAAE,CAAC;IAElC,+EAA+E;IAC/E,8DAA8D;IAC9D,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7C,SAAS,CAAE,MAAM,CAAC,MAAiB,IAAI,IAAI,CAAC,CAAC;QAC7C,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC;QACjC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACrB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,GAAG,GAAG,SAAS,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;IAC/C,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;IAClB,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/app.test.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { createApp } from './app.js';
|
|
6
|
+
import { ConfigStore } from './core/config.js';
|
|
7
|
+
import { CONFIG_DEFAULTS, CONFIG_VERSION } from './core/configSchema.js';
|
|
8
|
+
// Build a minimal valid AppConfig from the shipped defaults + a (fake) discord
|
|
9
|
+
// section. No realistic secret-shaped literals — placeholder ids only.
|
|
10
|
+
function makeConfig(overrides = {}) {
|
|
11
|
+
return {
|
|
12
|
+
...CONFIG_DEFAULTS,
|
|
13
|
+
version: CONFIG_VERSION,
|
|
14
|
+
discord: { token: 'fake-token-value', clientId: 'client-id-000' },
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function fakeClient() {
|
|
19
|
+
const onceHandlers = new Map();
|
|
20
|
+
const state = { loginCalls: [], destroyed: false };
|
|
21
|
+
const ready = {
|
|
22
|
+
user: { tag: 'bot#0001' },
|
|
23
|
+
guilds: { cache: new Map() },
|
|
24
|
+
};
|
|
25
|
+
const client = {
|
|
26
|
+
once: (event, handler) => {
|
|
27
|
+
onceHandlers.set(event, handler);
|
|
28
|
+
},
|
|
29
|
+
on: () => { },
|
|
30
|
+
login: async (token) => {
|
|
31
|
+
state.loginCalls.push(token);
|
|
32
|
+
},
|
|
33
|
+
destroy: async () => {
|
|
34
|
+
state.destroyed = true;
|
|
35
|
+
},
|
|
36
|
+
// resolveOverClient uses channels.fetch; onReady uses guilds.cache.size.
|
|
37
|
+
channels: { fetch: async () => null },
|
|
38
|
+
guilds: { cache: { size: 0 } },
|
|
39
|
+
token: 'fake-token-value',
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
client,
|
|
43
|
+
loginCalls: state.loginCalls,
|
|
44
|
+
get destroyed() {
|
|
45
|
+
return state.destroyed;
|
|
46
|
+
},
|
|
47
|
+
fireReady: async () => {
|
|
48
|
+
const handler = onceHandlers.get('ready') ?? onceHandlers.get('clientReady');
|
|
49
|
+
if (!handler)
|
|
50
|
+
throw new Error('no ClientReady handler was registered');
|
|
51
|
+
handler(ready);
|
|
52
|
+
// handleReady runs onReady asynchronously; let its microtasks flush.
|
|
53
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
describe('createApp — composition root', () => {
|
|
58
|
+
let dir;
|
|
59
|
+
let store;
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dab-app-'));
|
|
62
|
+
store = new ConfigStore(dir);
|
|
63
|
+
store.save(makeConfig());
|
|
64
|
+
});
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
67
|
+
});
|
|
68
|
+
it('constructs the full graph with a fake client without throwing', () => {
|
|
69
|
+
const fc = fakeClient();
|
|
70
|
+
const app = createApp({ config: makeConfig(), configStore: store, client: fc.client });
|
|
71
|
+
expect(app.orchestrator).toBeDefined();
|
|
72
|
+
expect(app.wiring).toBeDefined();
|
|
73
|
+
expect(app.usageService).toBeDefined();
|
|
74
|
+
expect(app.discord).toBeDefined();
|
|
75
|
+
});
|
|
76
|
+
it('registers BOTH the Claude and Codex modes in the mode registry', () => {
|
|
77
|
+
const fc = fakeClient();
|
|
78
|
+
const app = createApp({ config: makeConfig(), configStore: store, client: fc.client });
|
|
79
|
+
expect(app.modeRegistry.has('claude')).toBe(true);
|
|
80
|
+
expect(app.modeRegistry.get('claude').name).toBe('claude');
|
|
81
|
+
expect(app.modeRegistry.has('codex')).toBe(true);
|
|
82
|
+
expect(app.modeRegistry.get('codex').name).toBe('codex');
|
|
83
|
+
// Both registered → both offered as /mode backend choices and in the wizard.
|
|
84
|
+
expect(app.modeRegistry.list().sort()).toEqual(['claude', 'codex']);
|
|
85
|
+
});
|
|
86
|
+
it('exposes Codex capabilities (no permission prompts, no usage panel, transcript UX)', () => {
|
|
87
|
+
const fc = fakeClient();
|
|
88
|
+
const app = createApp({ config: makeConfig(), configStore: store, client: fc.client });
|
|
89
|
+
const caps = app.modeRegistry.get('codex').capabilities;
|
|
90
|
+
expect(caps.permissionPrompts).toBe(false);
|
|
91
|
+
expect(caps.usagePanel).toBe(false);
|
|
92
|
+
expect(caps.transcript).toBe(true);
|
|
93
|
+
expect(caps.streaming).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
it('wires orchestrator.requestPermission to the wiring layer (denies when unwired)', async () => {
|
|
96
|
+
const fc = fakeClient();
|
|
97
|
+
const app = createApp({ config: makeConfig(), configStore: store, client: fc.client });
|
|
98
|
+
// The orchestrator's requestPermission is the wiring hook: with no channel wired,
|
|
99
|
+
// it fails safe (deny) — proving the hook is the wiring's, not the default stub.
|
|
100
|
+
const decision = await app.wiring.requestPermission({ guildId: 'g1', channelId: 'c1', ownerId: 'u1' }, { toolName: 'Bash', input: {} });
|
|
101
|
+
expect(decision.behavior).toBe('deny');
|
|
102
|
+
});
|
|
103
|
+
it('login() forwards the config token to the client', async () => {
|
|
104
|
+
const fc = fakeClient();
|
|
105
|
+
const app = createApp({ config: makeConfig(), configStore: store, client: fc.client });
|
|
106
|
+
await app.login();
|
|
107
|
+
expect(fc.loginCalls).toEqual(['fake-token-value']);
|
|
108
|
+
});
|
|
109
|
+
it('onReady triggers orchestrator.resumeAll()', async () => {
|
|
110
|
+
const fc = fakeClient();
|
|
111
|
+
const app = createApp({ config: makeConfig(), configStore: store, client: fc.client });
|
|
112
|
+
const spy = vi.spyOn(app.orchestrator, 'resumeAll');
|
|
113
|
+
await fc.fireReady();
|
|
114
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
//# sourceMappingURL=app.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.test.js","sourceRoot":"","sources":["../src/app.test.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { startBot } from './app.js';
|
|
6
|
+
import { runSetup } from './setup/wizard.js';
|
|
7
|
+
import { ConfigStore } from './core/config.js';
|
|
8
|
+
// Thin entrypoint (§4): `--version` prints the package version, `--setup` runs the
|
|
9
|
+
// first-run wizard only (explicit re-configure), and the default/no-flag path is a
|
|
10
|
+
// one-command first-run — it runs the wizard automatically when nothing is
|
|
11
|
+
// configured yet, then boots the bot. All real work lives in app.ts /
|
|
12
|
+
// setup/wizard.ts; this file only parses argv, decides, and dispatches.
|
|
13
|
+
// Read the package version from package.json (two levels up from dist/cli.js, and
|
|
14
|
+
// from src/cli.ts in dev). Kept as a function so a test can call it directly.
|
|
15
|
+
export function readVersion() {
|
|
16
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
// dist/cli.js → ../package.json ; src/cli.ts → ../package.json
|
|
18
|
+
const pkgPath = path.join(here, '..', 'package.json');
|
|
19
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
20
|
+
return pkg.version ?? '0.0.0';
|
|
21
|
+
}
|
|
22
|
+
// Is this a first run that needs the wizard? True when there is no config.json yet,
|
|
23
|
+
// OR a config exists but its Discord token is empty/whitespace — the same two
|
|
24
|
+
// EXPECTED first-run conditions startBot() treats as "not set up" (the friendly
|
|
25
|
+
// no-token check added by the friendly-startup fix). A present-but-INVALID config
|
|
26
|
+
// is deliberately NOT treated as needs-setup: load() throws, and we let that
|
|
27
|
+
// propagate to startBot() so a real corruption bug surfaces (it stays the friendly
|
|
28
|
+
// fallback's job to report a still-can't-start config).
|
|
29
|
+
function needsSetup(store) {
|
|
30
|
+
if (!store.exists())
|
|
31
|
+
return true;
|
|
32
|
+
return store.load().discord.token.trim().length === 0;
|
|
33
|
+
}
|
|
34
|
+
export async function run(argv) {
|
|
35
|
+
if (argv.includes('--version')) {
|
|
36
|
+
console.log(readVersion());
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Explicit re-configure: run the wizard ONLY, never auto-start afterwards.
|
|
40
|
+
if (argv.includes('--setup')) {
|
|
41
|
+
await runSetup();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// No flags: one-command first-run. When nothing is configured yet, run the wizard
|
|
45
|
+
// and then start the bot in the same process; when already configured, start
|
|
46
|
+
// directly. If setup was cancelled (or still wrote nothing usable), startBot's
|
|
47
|
+
// own guardrails print the friendly "run --setup" fallback and exit non-zero.
|
|
48
|
+
if (needsSetup(new ConfigStore())) {
|
|
49
|
+
await runSetup();
|
|
50
|
+
console.log('설정 완료. 봇을 시작합니다…');
|
|
51
|
+
}
|
|
52
|
+
await startBot();
|
|
53
|
+
}
|
|
54
|
+
// Only auto-run when invoked as the CLI, not when imported by a test.
|
|
55
|
+
const isMain = typeof process.argv[1] === 'string' &&
|
|
56
|
+
import.meta.url === new URL(`file://${process.argv[1]}`).href;
|
|
57
|
+
if (isMain) {
|
|
58
|
+
run(process.argv.slice(2)).catch((err) => {
|
|
59
|
+
console.error(err);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,mFAAmF;AACnF,mFAAmF;AACnF,2EAA2E;AAC3E,sEAAsE;AACtE,wEAAwE;AAExE,kFAAkF;AAClF,8EAA8E;AAC9E,MAAM,UAAU,WAAW;IACzB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1D,+DAA+D;IAC/D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;IACtD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAyB,CAAC;IAClF,OAAO,GAAG,CAAC,OAAO,IAAI,OAAO,CAAC;AAChC,CAAC;AAED,oFAAoF;AACpF,8EAA8E;AAC9E,gFAAgF;AAChF,kFAAkF;AAClF,6EAA6E;AAC7E,mFAAmF;AACnF,wDAAwD;AACxD,SAAS,UAAU,CAAC,KAAkB;IACpC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;QAAE,OAAO,IAAI,CAAC;IACjC,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,IAAc;IACtC,IAAI,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;QAC3B,OAAO;IACT,CAAC;IACD,2EAA2E;IAC3E,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7B,MAAM,QAAQ,EAAE,CAAC;QACjB,OAAO;IACT,CAAC;IACD,kFAAkF;IAClF,6EAA6E;IAC7E,+EAA+E;IAC/E,8EAA8E;IAC9E,IAAI,UAAU,CAAC,IAAI,WAAW,EAAE,CAAC,EAAE,CAAC;QAClC,MAAM,QAAQ,EAAE,CAAC;QACjB,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAClC,CAAC;IACD,MAAM,QAAQ,EAAE,CAAC;AACnB,CAAC;AAED,sEAAsE;AACtE,MAAM,MAAM,GACV,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ;IACnC,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,IAAI,GAAG,CAAC,UAAU,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC;AAEhE,IAAI,MAAM,EAAE,CAAC;IACX,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACvC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC"}
|