companionbot 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DinN0000
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,149 @@
1
+ # CompanionBot
2
+
3
+ Claude 기반의 개인화된 페르소나를 가진 AI Companion Bot
4
+
5
+ ## 기능
6
+
7
+ - 자연스러운 대화 (Claude Sonnet/Opus/Haiku)
8
+ - 첫 실행 시 온보딩으로 페르소나 설정
9
+ - 이미지 분석 (사진 보내면 분석)
10
+ - 링크 요약 (URL 보내면 내용 요약)
11
+ - 날씨 조회 ("서울 날씨 어때?")
12
+ - 리마인더 ("10분 뒤에 알려줘")
13
+ - Google Calendar 연동
14
+ - 일일 브리핑 (매일 아침 날씨/일정)
15
+ - Heartbeat (주기적 체크 후 알림)
16
+ - 일일 메모리 자동 저장
17
+
18
+ ## 설치
19
+
20
+ ### 간편 설치 (일반 사용자)
21
+
22
+ ```bash
23
+ npm install -g companionbot
24
+ companionbot
25
+ ```
26
+
27
+ 첫 실행 시 자동으로 설정을 안내합니다.
28
+
29
+ ### 개발자 설치 (소스코드 수정)
30
+
31
+ ```bash
32
+ git clone https://github.com/hwai/companionbot.git
33
+ cd companionbot
34
+ npm install
35
+ npm run build
36
+ npm start
37
+ ```
38
+
39
+ ### 사전 준비
40
+
41
+ - **Node.js 18+**
42
+ - **Telegram Bot Token** - @BotFather에서 발급
43
+ - **Anthropic API Key** - console.anthropic.com
44
+
45
+ #### Linux 사용자 (keytar 의존성)
46
+
47
+ ```bash
48
+ # Debian/Ubuntu
49
+ sudo apt-get install libsecret-1-dev
50
+
51
+ # Fedora
52
+ sudo dnf install libsecret-devel
53
+
54
+ # Arch
55
+ sudo pacman -S libsecret
56
+ ```
57
+
58
+ ## 첫 실행
59
+
60
+ ```
61
+ 🤖 CompanionBot 첫 실행입니다!
62
+
63
+ [1/2] Telegram Bot Token
64
+ @BotFather에서 봇 생성 후 토큰을 붙여넣으세요.
65
+ Token: _
66
+
67
+ [2/2] Anthropic API Key
68
+ console.anthropic.com에서 발급받으세요.
69
+ API Key: _
70
+
71
+ 📁 워크스페이스 생성 중...
72
+ → ~/.companionbot/ 생성 완료
73
+
74
+ 🚀 봇을 시작합니다!
75
+ ```
76
+
77
+ ## 명령어
78
+
79
+ | 명령어 | 설명 |
80
+ |--------|------|
81
+ | `/start` | 봇 시작 (첫 실행 시 온보딩) |
82
+ | `/setup` | 기능 설정 메뉴 |
83
+ | `/briefing` | 일일 브리핑 토글 |
84
+ | `/heartbeat` | Heartbeat 토글 |
85
+ | `/reminders` | 알림 목록 |
86
+ | `/calendar` | 오늘 일정 |
87
+ | `/compact` | 대화 정리 |
88
+ | `/memory` | 최근 기억 |
89
+ | `/reset` | 페르소나 초기화 |
90
+
91
+ ### 자연어 명령
92
+
93
+ 명령어 대신 자연어로 말해도 됩니다:
94
+
95
+ - "하이쿠로 바꿔줘" → 모델 변경
96
+ - "10분 뒤에 알려줘" → 리마인더
97
+ - "브리핑 꺼줘" → 브리핑 비활성화
98
+ - "아침 9시에 브리핑 해줘" → 브리핑 시간 설정
99
+ - "지금 브리핑 해줘" → 즉시 브리핑
100
+ - "하트비트 켜줘" → Heartbeat 활성화
101
+ - "서울 날씨 어때?" → 날씨 조회
102
+ - "이거 기억해둬" → 메모리 저장
103
+
104
+ ## PM2로 상시 실행
105
+
106
+ ```bash
107
+ npm install -g pm2
108
+ pm2 start npm --name companionbot -- start
109
+ pm2 startup && pm2 save
110
+ ```
111
+
112
+ ## 워크스페이스
113
+
114
+ `~/.companionbot/` 구조:
115
+
116
+ ```
117
+ ├── AGENTS.md # 운영 지침
118
+ ├── BOOTSTRAP.md # 온보딩 (완료 후 삭제)
119
+ ├── HEARTBEAT.md # 주기적 체크 항목
120
+ ├── IDENTITY.md # 봇 정체성
121
+ ├── MEMORY.md # 장기 기억
122
+ ├── SOUL.md # 봇 성격
123
+ ├── TOOLS.md # 도구 설정
124
+ ├── USER.md # 사용자 정보
125
+ ├── canvas/ # 봇 작업 디렉토리
126
+ └── memory/ # 일일 로그
127
+ └── YYYY-MM-DD.md
128
+ ```
129
+
130
+ ## 시크릿 저장
131
+
132
+ OS 키체인에 안전하게 저장됩니다:
133
+ - macOS: Keychain Access
134
+ - Windows: Credential Manager
135
+ - Linux: libsecret
136
+
137
+ 재설정: `~/.companionbot/` 삭제 후 다시 실행
138
+
139
+ ## 개발
140
+
141
+ ```bash
142
+ npm run dev # 개발 모드
143
+ npm run build # 빌드
144
+ npm start # 실행
145
+ ```
146
+
147
+ ## License
148
+
149
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/cli/main.js";
@@ -0,0 +1,104 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import { tools, executeTool } from "../tools/index.js";
3
+ let anthropic = null;
4
+ function getClient() {
5
+ if (!anthropic) {
6
+ anthropic = new Anthropic();
7
+ }
8
+ return anthropic;
9
+ }
10
+ export const MODELS = {
11
+ sonnet: { id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4" },
12
+ opus: { id: "claude-opus-4-20250514", name: "Claude Opus 4" },
13
+ haiku: { id: "claude-haiku-3-5-20241022", name: "Claude Haiku 3.5" },
14
+ };
15
+ export async function chat(messages, systemPrompt, modelId = "sonnet") {
16
+ const client = getClient();
17
+ const model = MODELS[modelId].id;
18
+ // 메시지를 API 형식으로 변환
19
+ const apiMessages = messages.map((m) => ({
20
+ role: m.role,
21
+ content: m.content,
22
+ }));
23
+ let response;
24
+ try {
25
+ response = await client.messages.create({
26
+ model,
27
+ max_tokens: 4096,
28
+ system: systemPrompt,
29
+ messages: apiMessages,
30
+ tools: tools,
31
+ });
32
+ }
33
+ catch (error) {
34
+ if (error instanceof Anthropic.APIError) {
35
+ if (error.status === 429) {
36
+ throw new Error("API 요청이 너무 많아. 잠시 후 다시 시도해줘.");
37
+ }
38
+ if (error.status >= 500) {
39
+ throw new Error("AI 서버에 문제가 생겼어. 잠시 후 다시 시도해줘.");
40
+ }
41
+ }
42
+ throw error;
43
+ }
44
+ // Tool use 루프 - Claude가 도구 사용을 멈출 때까지 반복 (최대 10회)
45
+ const MAX_TOOL_ITERATIONS = 10;
46
+ let iterations = 0;
47
+ while (response.stop_reason === "tool_use" && iterations < MAX_TOOL_ITERATIONS) {
48
+ iterations++;
49
+ const toolUseBlocks = response.content.filter((block) => block.type === "tool_use");
50
+ // 도구 실행 결과 수집
51
+ const toolResults = [];
52
+ for (const toolUse of toolUseBlocks) {
53
+ console.log(`[Tool] ${toolUse.name}:`, JSON.stringify(toolUse.input));
54
+ const result = await executeTool(toolUse.name, toolUse.input);
55
+ // 결과가 너무 길면 자르기
56
+ const truncatedResult = result.length > 10000
57
+ ? result.slice(0, 10000) + "\n... (truncated)"
58
+ : result;
59
+ toolResults.push({
60
+ type: "tool_result",
61
+ tool_use_id: toolUse.id,
62
+ content: truncatedResult,
63
+ });
64
+ }
65
+ // 어시스턴트 메시지와 도구 결과 추가
66
+ apiMessages.push({
67
+ role: "assistant",
68
+ content: response.content,
69
+ });
70
+ apiMessages.push({
71
+ role: "user",
72
+ content: toolResults,
73
+ });
74
+ // 다음 응답 요청
75
+ try {
76
+ response = await client.messages.create({
77
+ model,
78
+ max_tokens: 4096,
79
+ system: systemPrompt,
80
+ messages: apiMessages,
81
+ tools: tools,
82
+ });
83
+ }
84
+ catch (error) {
85
+ if (error instanceof Anthropic.APIError) {
86
+ if (error.status === 429) {
87
+ throw new Error("API 요청이 너무 많아. 잠시 후 다시 시도해줘.");
88
+ }
89
+ if (error.status >= 500) {
90
+ throw new Error("AI 서버에 문제가 생겼어. 잠시 후 다시 시도해줘.");
91
+ }
92
+ }
93
+ throw error;
94
+ }
95
+ }
96
+ // 반복 횟수 초과 시 경고
97
+ if (iterations >= MAX_TOOL_ITERATIONS) {
98
+ console.warn(`[Warning] Tool use loop reached max iterations (${MAX_TOOL_ITERATIONS})`);
99
+ return "도구 실행이 너무 많이 반복됐어. 다시 시도해줄래?";
100
+ }
101
+ // 최종 텍스트 응답 추출
102
+ const textBlock = response.content.find((block) => block.type === "text");
103
+ return textBlock?.text ?? "응답을 생성하지 못했어. 다시 시도해줄래?";
104
+ }
@@ -0,0 +1,202 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ import cron from "node-cron";
4
+ import { getWorkspacePath } from "../workspace/index.js";
5
+ import { getSecret } from "../config/secrets.js";
6
+ import { isCalendarConfigured, getTodayEvents, formatEvent } from "../calendar/index.js";
7
+ // 활성 스케줄
8
+ const activeJobs = new Map();
9
+ // 봇 인스턴스
10
+ let botInstance = null;
11
+ export function setBriefingBot(bot) {
12
+ botInstance = bot;
13
+ }
14
+ function getConfigPath() {
15
+ return path.join(getWorkspacePath(), "briefing.json");
16
+ }
17
+ async function loadStore() {
18
+ try {
19
+ const data = await fs.readFile(getConfigPath(), "utf-8");
20
+ return JSON.parse(data);
21
+ }
22
+ catch {
23
+ return { configs: [] };
24
+ }
25
+ }
26
+ async function saveStore(store) {
27
+ await fs.writeFile(getConfigPath(), JSON.stringify(store, null, 2));
28
+ }
29
+ // 날씨 가져오기
30
+ async function fetchWeather(city) {
31
+ const apiKey = await getSecret("openweathermap-api-key");
32
+ if (!apiKey)
33
+ return null;
34
+ try {
35
+ const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=${apiKey}&units=metric&lang=kr`;
36
+ const response = await fetch(url);
37
+ const data = await response.json();
38
+ if (data.cod !== 200)
39
+ return null;
40
+ const temp = Math.round(data.main.temp);
41
+ const description = data.weather[0].description;
42
+ const icon = getWeatherEmoji(data.weather[0].icon);
43
+ return `${icon} ${city} ${temp}°C, ${description}`;
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ function getWeatherEmoji(iconCode) {
50
+ const map = {
51
+ "01d": "☀️", "01n": "🌙",
52
+ "02d": "⛅", "02n": "☁️",
53
+ "03d": "☁️", "03n": "☁️",
54
+ "04d": "☁️", "04n": "☁️",
55
+ "09d": "🌧️", "09n": "🌧️",
56
+ "10d": "🌦️", "10n": "🌧️",
57
+ "11d": "⛈️", "11n": "⛈️",
58
+ "13d": "❄️", "13n": "❄️",
59
+ "50d": "🌫️", "50n": "🌫️",
60
+ };
61
+ return map[iconCode] || "🌤️";
62
+ }
63
+ // 브리핑 실행
64
+ async function executeBriefing(config) {
65
+ if (!botInstance) {
66
+ console.error("[Briefing] Bot instance not set");
67
+ return;
68
+ }
69
+ const parts = [];
70
+ // 인사
71
+ const hour = new Date().getHours();
72
+ const greeting = hour < 12 ? "좋은 아침!" : hour < 18 ? "좋은 오후!" : "좋은 저녁!";
73
+ parts.push(`☀️ ${greeting}\n`);
74
+ // 날씨
75
+ const weather = await fetchWeather(config.city);
76
+ if (weather) {
77
+ parts.push(`🌤️ 오늘 날씨\n${weather}\n`);
78
+ }
79
+ // 캘린더
80
+ const calendarConfigured = await isCalendarConfigured();
81
+ if (calendarConfigured) {
82
+ try {
83
+ const events = await getTodayEvents();
84
+ if (events.length > 0) {
85
+ const eventList = events.slice(0, 5).map(formatEvent).join("\n• ");
86
+ parts.push(`📅 오늘 일정\n• ${eventList}\n`);
87
+ }
88
+ else {
89
+ parts.push(`📅 오늘 일정 없음\n`);
90
+ }
91
+ }
92
+ catch (error) {
93
+ console.error("[Briefing] Calendar error:", error);
94
+ }
95
+ }
96
+ // 마무리
97
+ parts.push(`좋은 하루 보내세요! 🙂`);
98
+ const message = parts.join("\n");
99
+ try {
100
+ await botInstance.api.sendMessage(config.chatId, message);
101
+ console.log(`[Briefing] Sent to ${config.chatId}`);
102
+ }
103
+ catch (error) {
104
+ console.error("[Briefing] Send error:", error);
105
+ }
106
+ }
107
+ // 스케줄 설정
108
+ function scheduleBriefing(config) {
109
+ // 기존 job 취소
110
+ const existing = activeJobs.get(config.chatId);
111
+ if (existing) {
112
+ existing.stop();
113
+ activeJobs.delete(config.chatId);
114
+ }
115
+ if (!config.enabled)
116
+ return;
117
+ const [hour, minute] = config.time.split(":").map(Number);
118
+ const cronExpr = `${minute} ${hour} * * *`;
119
+ const job = cron.schedule(cronExpr, () => {
120
+ executeBriefing(config);
121
+ }, {
122
+ timezone: config.timezone,
123
+ });
124
+ activeJobs.set(config.chatId, job);
125
+ console.log(`[Briefing] Scheduled for ${config.chatId} at ${config.time}`);
126
+ }
127
+ // 브리핑 설정
128
+ export async function setBriefingConfig(chatId, enabled, time = "08:00", city = "Seoul", timezone = "Asia/Seoul") {
129
+ const store = await loadStore();
130
+ const existingIndex = store.configs.findIndex((c) => c.chatId === chatId);
131
+ const config = {
132
+ chatId,
133
+ enabled,
134
+ time,
135
+ city,
136
+ timezone,
137
+ };
138
+ if (existingIndex >= 0) {
139
+ store.configs[existingIndex] = config;
140
+ }
141
+ else {
142
+ store.configs.push(config);
143
+ }
144
+ await saveStore(store);
145
+ scheduleBriefing(config);
146
+ return config;
147
+ }
148
+ // 브리핑 설정 가져오기
149
+ export async function getBriefingConfig(chatId) {
150
+ const store = await loadStore();
151
+ return store.configs.find((c) => c.chatId === chatId) || null;
152
+ }
153
+ // 브리핑 비활성화
154
+ export async function disableBriefing(chatId) {
155
+ const store = await loadStore();
156
+ const config = store.configs.find((c) => c.chatId === chatId);
157
+ if (config) {
158
+ config.enabled = false;
159
+ await saveStore(store);
160
+ const job = activeJobs.get(chatId);
161
+ if (job) {
162
+ job.stop();
163
+ activeJobs.delete(chatId);
164
+ }
165
+ }
166
+ }
167
+ // 모든 브리핑 복원 (봇 시작 시)
168
+ export async function restoreBriefings() {
169
+ const store = await loadStore();
170
+ for (const config of store.configs) {
171
+ if (config.enabled) {
172
+ scheduleBriefing(config);
173
+ }
174
+ }
175
+ console.log(`[Briefing] Restored ${activeJobs.size} briefings`);
176
+ }
177
+ // 즉시 브리핑 실행 (테스트용)
178
+ export async function sendBriefingNow(chatId) {
179
+ const config = await getBriefingConfig(chatId);
180
+ if (!config) {
181
+ // 기본 설정으로 실행
182
+ const defaultConfig = {
183
+ chatId,
184
+ enabled: false,
185
+ time: "08:00",
186
+ city: "Seoul",
187
+ timezone: "Asia/Seoul",
188
+ };
189
+ await executeBriefing(defaultConfig);
190
+ return true;
191
+ }
192
+ await executeBriefing(config);
193
+ return true;
194
+ }
195
+ // 모든 스케줄 정리 (graceful shutdown)
196
+ export function cleanupBriefings() {
197
+ for (const [chatId, job] of activeJobs) {
198
+ job.stop();
199
+ }
200
+ activeJobs.clear();
201
+ console.log("[Briefing] Cleanup complete");
202
+ }