companionbot 0.1.1 → 0.2.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.
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Sub-agent 시스템 - 복잡한 작업을 독립적인 agent에게 위임
3
+ */
4
+ export { setAgentBot, spawnAgent, listAgents, cancelAgent, getAgent } from "./manager.js";
@@ -0,0 +1,171 @@
1
+ /**
2
+ * AgentManager - Sub-agent 생성 및 관리
3
+ *
4
+ * 각 sub-agent는:
5
+ * - 별도의 Claude API 호출로 독립 실행
6
+ * - 메인 conversation과 별개의 context
7
+ * - 비동기로 실행, 완료 시 callback
8
+ */
9
+ import Anthropic from "@anthropic-ai/sdk";
10
+ import { randomUUID } from "crypto";
11
+ // Agent 저장소
12
+ const agents = new Map();
13
+ // Bot 인스턴스 (결과 전송용)
14
+ let botInstance = null;
15
+ // Anthropic 클라이언트
16
+ let anthropic = null;
17
+ function getClient() {
18
+ if (!anthropic) {
19
+ anthropic = new Anthropic();
20
+ }
21
+ return anthropic;
22
+ }
23
+ /**
24
+ * Bot 인스턴스 설정 (시작 시 호출)
25
+ */
26
+ export function setAgentBot(bot) {
27
+ botInstance = bot;
28
+ }
29
+ /**
30
+ * Sub-agent 생성 및 실행
31
+ */
32
+ export async function spawnAgent(task, chatId) {
33
+ const id = randomUUID().slice(0, 8);
34
+ const agent = {
35
+ id,
36
+ task,
37
+ status: "running",
38
+ chatId,
39
+ createdAt: new Date(),
40
+ };
41
+ agents.set(id, agent);
42
+ // 비동기로 agent 실행 (await 하지 않음)
43
+ runAgent(agent).catch((err) => {
44
+ console.error(`[Agent ${id}] Error:`, err);
45
+ });
46
+ return id;
47
+ }
48
+ /**
49
+ * Agent 실행 (내부 함수)
50
+ */
51
+ async function runAgent(agent) {
52
+ const client = getClient();
53
+ const systemPrompt = `You are a sub-agent assistant. Your job is to complete a specific task and report the result concisely.
54
+
55
+ TASK: ${agent.task}
56
+
57
+ Guidelines:
58
+ - Focus only on the given task
59
+ - Be concise but thorough
60
+ - Report results clearly
61
+ - If you cannot complete the task, explain why
62
+
63
+ Complete the task and provide your final answer.`;
64
+ try {
65
+ console.log(`[Agent ${agent.id}] Starting: ${agent.task.slice(0, 50)}...`);
66
+ const response = await client.messages.create({
67
+ model: "claude-sonnet-4-20250514",
68
+ max_tokens: 2048,
69
+ system: systemPrompt,
70
+ messages: [
71
+ {
72
+ role: "user",
73
+ content: `Please complete this task: ${agent.task}`,
74
+ },
75
+ ],
76
+ });
77
+ // 결과 추출
78
+ const textBlock = response.content.find((block) => block.type === "text");
79
+ const result = textBlock?.text ?? "No response generated.";
80
+ // Agent 상태 업데이트
81
+ agent.status = "completed";
82
+ agent.completedAt = new Date();
83
+ agent.result = result;
84
+ console.log(`[Agent ${agent.id}] Completed`);
85
+ // 결과를 원래 chat에 전송
86
+ await sendAgentResult(agent);
87
+ }
88
+ catch (error) {
89
+ agent.status = "failed";
90
+ agent.completedAt = new Date();
91
+ agent.error = error instanceof Error ? error.message : String(error);
92
+ console.error(`[Agent ${agent.id}] Failed:`, agent.error);
93
+ // 실패도 알림
94
+ await sendAgentResult(agent);
95
+ }
96
+ }
97
+ /**
98
+ * Agent 결과를 chat에 전송
99
+ */
100
+ async function sendAgentResult(agent) {
101
+ if (!botInstance) {
102
+ console.warn("[Agent] No bot instance, cannot send result");
103
+ return;
104
+ }
105
+ let message;
106
+ if (agent.status === "completed") {
107
+ message = `🤖 **Sub-agent 완료** (${agent.id})\n\n📋 Task: ${agent.task.slice(0, 100)}${agent.task.length > 100 ? "..." : ""}\n\n✅ Result:\n${agent.result}`;
108
+ }
109
+ else if (agent.status === "failed") {
110
+ message = `🤖 **Sub-agent 실패** (${agent.id})\n\n📋 Task: ${agent.task.slice(0, 100)}${agent.task.length > 100 ? "..." : ""}\n\n❌ Error: ${agent.error}`;
111
+ }
112
+ else if (agent.status === "cancelled") {
113
+ message = `🤖 **Sub-agent 취소됨** (${agent.id})`;
114
+ }
115
+ else {
116
+ return; // running 상태면 전송 안 함
117
+ }
118
+ try {
119
+ await botInstance.api.sendMessage(agent.chatId, message);
120
+ }
121
+ catch (err) {
122
+ console.error(`[Agent ${agent.id}] Failed to send result:`, err);
123
+ }
124
+ }
125
+ /**
126
+ * Agent 목록 조회
127
+ */
128
+ export function listAgents(chatId) {
129
+ const allAgents = Array.from(agents.values());
130
+ if (chatId !== undefined) {
131
+ return allAgents.filter((a) => a.chatId === chatId);
132
+ }
133
+ return allAgents;
134
+ }
135
+ /**
136
+ * Agent 취소
137
+ */
138
+ export function cancelAgent(agentId) {
139
+ const agent = agents.get(agentId);
140
+ if (!agent) {
141
+ return false;
142
+ }
143
+ if (agent.status !== "running") {
144
+ return false; // 이미 완료된 agent는 취소 불가
145
+ }
146
+ // 실제로 실행 중인 API 호출을 취소할 수는 없지만
147
+ // 상태를 cancelled로 표시하고 결과 전송 시 무시되도록 함
148
+ agent.status = "cancelled";
149
+ agent.completedAt = new Date();
150
+ console.log(`[Agent ${agentId}] Cancelled`);
151
+ return true;
152
+ }
153
+ /**
154
+ * Agent 상태 조회
155
+ */
156
+ export function getAgent(agentId) {
157
+ return agents.get(agentId);
158
+ }
159
+ /**
160
+ * 오래된 agent 정리 (1시간 이상)
161
+ */
162
+ export function cleanupOldAgents() {
163
+ const oneHourAgo = Date.now() - 60 * 60 * 1000;
164
+ for (const [id, agent] of agents.entries()) {
165
+ if (agent.completedAt && agent.completedAt.getTime() < oneHourAgo) {
166
+ agents.delete(id);
167
+ }
168
+ }
169
+ }
170
+ // 10분마다 정리
171
+ setInterval(cleanupOldAgents, 10 * 60 * 1000);
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Sub-agent 시스템 타입 정의
3
+ */
4
+ export {};
@@ -2,6 +2,7 @@ import { Bot } from "grammy";
2
2
  import { setBotInstance, restoreReminders } from "../reminders/index.js";
3
3
  import { setBriefingBot, restoreBriefings } from "../briefing/index.js";
4
4
  import { setHeartbeatBot, restoreHeartbeats } from "../heartbeat/index.js";
5
+ import { setAgentBot } from "../agents/index.js";
5
6
  import { registerCommands, registerMessageHandlers } from "./handlers/index.js";
6
7
  // Re-export for external use
7
8
  export { invalidateWorkspaceCache } from "./utils/index.js";
@@ -19,6 +20,8 @@ export function createBot(token) {
19
20
  // Heartbeat 초기화
20
21
  setHeartbeatBot(bot);
21
22
  restoreHeartbeats().catch((err) => console.error("Failed to restore heartbeats:", err));
23
+ // Sub-agent 시스템 초기화
24
+ setAgentBot(bot);
22
25
  // 에러 핸들링
23
26
  bot.catch((err) => {
24
27
  console.error("Bot error:", err);
@@ -1,8 +1,9 @@
1
1
  import * as fs from "fs/promises";
2
2
  import * as fsSync from "fs";
3
3
  import * as path from "path";
4
- import { exec } from "child_process";
4
+ import { exec, spawn } from "child_process";
5
5
  import { promisify } from "util";
6
+ import { randomUUID } from "crypto";
6
7
  import { MODELS } from "../ai/claude.js";
7
8
  import { getCurrentChatId, setModel, getModel } from "../session/state.js";
8
9
  import { getWorkspacePath, saveWorkspaceFile, appendToMemory, deleteBootstrap, } from "../workspace/index.js";
@@ -11,7 +12,20 @@ import { createReminder, deleteReminder, getReminders, parseTimeExpression, } fr
11
12
  import { isCalendarConfigured, getEvents, addEvent, deleteEvent, formatEvent, parseDateExpression, } from "../calendar/index.js";
12
13
  import { setHeartbeatConfig, getHeartbeatConfig, disableHeartbeat, runHeartbeatNow, } from "../heartbeat/index.js";
13
14
  import { setBriefingConfig, getBriefingConfig, disableBriefing, sendBriefingNow, } from "../briefing/index.js";
15
+ import { spawnAgent, listAgents, cancelAgent, } from "../agents/index.js";
14
16
  const execAsync = promisify(exec);
17
+ // 메모리에 세션 저장
18
+ const sessions = new Map();
19
+ // Output buffer 최대 크기 (라인 수)
20
+ const MAX_OUTPUT_LINES = 1000;
21
+ function appendOutput(session, data) {
22
+ const lines = data.split("\n");
23
+ session.outputBuffer.push(...lines);
24
+ // 버퍼 크기 제한
25
+ if (session.outputBuffer.length > MAX_OUTPUT_LINES) {
26
+ session.outputBuffer = session.outputBuffer.slice(-MAX_OUTPUT_LINES);
27
+ }
28
+ }
15
29
  // 홈 디렉토리
16
30
  const home = process.env.HOME || "";
17
31
  // 허용된 디렉토리 (보안을 위해 제한)
@@ -118,7 +132,13 @@ export const tools = [
118
132
  },
119
133
  {
120
134
  name: "run_command",
121
- description: "Run a shell command. Use with caution. Only for safe commands like git status, npm run, etc.",
135
+ description: `Run a shell command. Use with caution. Only for safe commands like git status, npm run, etc.
136
+
137
+ When background=true:
138
+ - Command runs in detached mode
139
+ - Returns a session ID immediately
140
+ - Use list_sessions, get_session_log, kill_session to manage
141
+ - Useful for long-running commands (npm run dev, servers, etc.)`,
122
142
  input_schema: {
123
143
  type: "object",
124
144
  properties: {
@@ -130,10 +150,70 @@ export const tools = [
130
150
  type: "string",
131
151
  description: "The working directory to run the command in (optional)",
132
152
  },
153
+ background: {
154
+ type: "boolean",
155
+ description: "Run in background and return session ID (default: false)",
156
+ },
157
+ timeout: {
158
+ type: "number",
159
+ description: "Timeout in seconds for foreground commands (default: 30)",
160
+ },
133
161
  },
134
162
  required: ["command"],
135
163
  },
136
164
  },
165
+ {
166
+ name: "list_sessions",
167
+ description: "List all background command sessions. Shows running and recently completed sessions.",
168
+ input_schema: {
169
+ type: "object",
170
+ properties: {
171
+ status: {
172
+ type: "string",
173
+ enum: ["all", "running", "completed"],
174
+ description: "Filter by status (default: all)",
175
+ },
176
+ },
177
+ required: [],
178
+ },
179
+ },
180
+ {
181
+ name: "get_session_log",
182
+ description: "Get the output log of a background session.",
183
+ input_schema: {
184
+ type: "object",
185
+ properties: {
186
+ session_id: {
187
+ type: "string",
188
+ description: "The session ID to get logs from",
189
+ },
190
+ tail: {
191
+ type: "number",
192
+ description: "Number of lines from the end (default: 50)",
193
+ },
194
+ },
195
+ required: ["session_id"],
196
+ },
197
+ },
198
+ {
199
+ name: "kill_session",
200
+ description: "Kill a running background session.",
201
+ input_schema: {
202
+ type: "object",
203
+ properties: {
204
+ session_id: {
205
+ type: "string",
206
+ description: "The session ID to kill",
207
+ },
208
+ signal: {
209
+ type: "string",
210
+ enum: ["SIGTERM", "SIGKILL", "SIGINT"],
211
+ description: "Signal to send (default: SIGTERM)",
212
+ },
213
+ },
214
+ required: ["session_id"],
215
+ },
216
+ },
137
217
  {
138
218
  name: "change_model",
139
219
  description: `Change the AI model for this conversation. Use this when the user asks to switch models, or when you determine a different model would be better suited for the task.
@@ -399,6 +479,56 @@ Use this when the user says things like:
399
479
  required: [],
400
480
  },
401
481
  },
482
+ // ============== Sub-Agent 도구 ==============
483
+ {
484
+ name: "spawn_agent",
485
+ description: `Create a sub-agent to handle a complex or time-consuming task independently.
486
+
487
+ The sub-agent will:
488
+ - Run in the background with its own Claude API context
489
+ - Complete the task independently
490
+ - Report results back to this chat when done
491
+
492
+ Use this for:
493
+ - Tasks that require deep focus or analysis
494
+ - Long-running research or summarization
495
+ - Work that can be done in parallel while you handle other things
496
+
497
+ Example: "서브에이전트한테 이 코드 분석 시켜줘"`,
498
+ input_schema: {
499
+ type: "object",
500
+ properties: {
501
+ task: {
502
+ type: "string",
503
+ description: "Detailed description of the task for the sub-agent",
504
+ },
505
+ },
506
+ required: ["task"],
507
+ },
508
+ },
509
+ {
510
+ name: "list_agents",
511
+ description: "List all sub-agents and their status (running, completed, failed, cancelled).",
512
+ input_schema: {
513
+ type: "object",
514
+ properties: {},
515
+ required: [],
516
+ },
517
+ },
518
+ {
519
+ name: "cancel_agent",
520
+ description: "Cancel a running sub-agent by its ID.",
521
+ input_schema: {
522
+ type: "object",
523
+ properties: {
524
+ agent_id: {
525
+ type: "string",
526
+ description: "The sub-agent ID to cancel",
527
+ },
528
+ },
529
+ required: ["agent_id"],
530
+ },
531
+ },
402
532
  ];
403
533
  // Tool 실행 함수
404
534
  export async function executeTool(name, input) {
@@ -434,6 +564,8 @@ export async function executeTool(name, input) {
434
564
  case "run_command": {
435
565
  const command = input.command;
436
566
  const cwd = input.cwd || path.join(home, "Documents");
567
+ const background = input.background || false;
568
+ const timeout = (input.timeout || 30) * 1000;
437
569
  // 화이트리스트 방식: 허용된 명령어만 실행
438
570
  const ALLOWED_COMMANDS = [
439
571
  "git", "npm", "npx", "node", "ls", "pwd", "cat", "head", "tail",
@@ -455,18 +587,66 @@ export async function executeTool(name, input) {
455
587
  if (dangerousArgs.some(arg => parts.includes(arg))) {
456
588
  return `Error: Dangerous argument detected.`;
457
589
  }
458
- try {
459
- // 환경 변수는 필요한 것만 화이트리스트로 전달 (민감 정보 노출 방지)
460
- const safeEnv = {
461
- PATH: process.env.PATH || "",
462
- HOME: process.env.HOME || "",
463
- USER: process.env.USER || "",
464
- LANG: process.env.LANG || "en_US.UTF-8",
465
- TERM: process.env.TERM || "xterm",
590
+ // 환경 변수는 필요한 것만 화이트리스트로 전달 (민감 정보 노출 방지)
591
+ const safeEnv = {
592
+ PATH: process.env.PATH || "",
593
+ HOME: process.env.HOME || "",
594
+ USER: process.env.USER || "",
595
+ LANG: process.env.LANG || "en_US.UTF-8",
596
+ TERM: process.env.TERM || "xterm",
597
+ };
598
+ // Background 실행
599
+ if (background) {
600
+ const sessionId = randomUUID().slice(0, 8);
601
+ const child = spawn("sh", ["-c", command], {
602
+ cwd,
603
+ env: safeEnv,
604
+ detached: true,
605
+ stdio: ["ignore", "pipe", "pipe"],
606
+ });
607
+ const session = {
608
+ id: sessionId,
609
+ pid: child.pid,
610
+ command,
611
+ cwd,
612
+ startTime: new Date(),
613
+ outputBuffer: [],
614
+ process: child,
615
+ status: "running",
466
616
  };
617
+ // stdout/stderr 캡처
618
+ child.stdout?.on("data", (data) => {
619
+ appendOutput(session, data.toString());
620
+ });
621
+ child.stderr?.on("data", (data) => {
622
+ appendOutput(session, `[stderr] ${data.toString()}`);
623
+ });
624
+ // 프로세스 종료 핸들링
625
+ child.on("close", (code) => {
626
+ session.endTime = new Date();
627
+ session.exitCode = code;
628
+ session.status = code === 0 ? "completed" : "error";
629
+ });
630
+ child.on("error", (err) => {
631
+ session.status = "error";
632
+ appendOutput(session, `[error] ${err.message}`);
633
+ });
634
+ // unref로 부모 프로세스와 분리
635
+ child.unref();
636
+ sessions.set(sessionId, session);
637
+ return `Background session started.
638
+ Session ID: ${sessionId}
639
+ PID: ${child.pid}
640
+ Command: ${command}
641
+ CWD: ${cwd}
642
+
643
+ Use list_sessions to see all sessions, get_session_log to view output, kill_session to terminate.`;
644
+ }
645
+ // Foreground 실행 (기존 방식)
646
+ try {
467
647
  const { stdout, stderr } = await execAsync(command, {
468
648
  cwd,
469
- timeout: 30000,
649
+ timeout,
470
650
  env: safeEnv,
471
651
  });
472
652
  return stdout || stderr || "Command executed (no output)";
@@ -475,6 +655,87 @@ export async function executeTool(name, input) {
475
655
  return `Error: ${error instanceof Error ? error.message : String(error)}`;
476
656
  }
477
657
  }
658
+ case "list_sessions": {
659
+ const statusFilter = input.status || "all";
660
+ const sessionList = [];
661
+ for (const [id, session] of sessions) {
662
+ // 상태 필터링
663
+ if (statusFilter !== "all") {
664
+ if (statusFilter === "running" && session.status !== "running")
665
+ continue;
666
+ if (statusFilter === "completed" && session.status === "running")
667
+ continue;
668
+ }
669
+ const runtime = session.endTime
670
+ ? `${Math.round((session.endTime.getTime() - session.startTime.getTime()) / 1000)}s`
671
+ : `${Math.round((Date.now() - session.startTime.getTime()) / 1000)}s (running)`;
672
+ const status = session.status === "running"
673
+ ? "🟢 running"
674
+ : session.status === "completed"
675
+ ? "✅ completed"
676
+ : session.status === "killed"
677
+ ? "🔴 killed"
678
+ : "❌ error";
679
+ sessionList.push(`[${id}] ${status}
680
+ Command: ${session.command}
681
+ PID: ${session.pid}
682
+ Runtime: ${runtime}
683
+ Exit code: ${session.exitCode ?? "N/A"}`);
684
+ }
685
+ if (sessionList.length === 0) {
686
+ return `No sessions found${statusFilter !== "all" ? ` with status "${statusFilter}"` : ""}.`;
687
+ }
688
+ return `Sessions (${sessionList.length}):\n\n${sessionList.join("\n\n")}`;
689
+ }
690
+ case "get_session_log": {
691
+ const sessionId = input.session_id;
692
+ const tail = input.tail || 50;
693
+ const session = sessions.get(sessionId);
694
+ if (!session) {
695
+ return `Error: Session "${sessionId}" not found. Use list_sessions to see available sessions.`;
696
+ }
697
+ const lines = session.outputBuffer.slice(-tail);
698
+ if (lines.length === 0) {
699
+ return `Session ${sessionId} has no output yet.
700
+ Status: ${session.status}
701
+ Command: ${session.command}`;
702
+ }
703
+ const header = `Session: ${sessionId} (${session.status})
704
+ Command: ${session.command}
705
+ Showing last ${lines.length} lines:
706
+ ${"─".repeat(40)}`;
707
+ return `${header}\n${lines.join("\n")}`;
708
+ }
709
+ case "kill_session": {
710
+ const sessionId = input.session_id;
711
+ const signal = input.signal || "SIGTERM";
712
+ const session = sessions.get(sessionId);
713
+ if (!session) {
714
+ return `Error: Session "${sessionId}" not found.`;
715
+ }
716
+ if (session.status !== "running") {
717
+ return `Session ${sessionId} is not running (status: ${session.status}).`;
718
+ }
719
+ try {
720
+ // Process group kill (negative PID)
721
+ process.kill(-session.pid, signal);
722
+ session.status = "killed";
723
+ session.endTime = new Date();
724
+ return `Session ${sessionId} (PID ${session.pid}) killed with ${signal}.`;
725
+ }
726
+ catch (error) {
727
+ // 단일 프로세스 kill 시도
728
+ try {
729
+ session.process.kill(signal);
730
+ session.status = "killed";
731
+ session.endTime = new Date();
732
+ return `Session ${sessionId} killed with ${signal}.`;
733
+ }
734
+ catch (e) {
735
+ return `Error killing session: ${error instanceof Error ? error.message : String(error)}`;
736
+ }
737
+ }
738
+ }
478
739
  case "change_model": {
479
740
  const modelId = input.model;
480
741
  const reason = input.reason || "";
@@ -750,6 +1011,52 @@ export async function executeTool(name, input) {
750
1011
  await sendBriefingNow(chatId);
751
1012
  return "Briefing sent!";
752
1013
  }
1014
+ // ============== Sub-Agent 도구 ==============
1015
+ case "spawn_agent": {
1016
+ const chatId = getCurrentChatId();
1017
+ if (!chatId) {
1018
+ return "Error: No active chat session";
1019
+ }
1020
+ const task = input.task;
1021
+ if (!task || task.trim().length === 0) {
1022
+ return "Error: Task description is required";
1023
+ }
1024
+ const agentId = await spawnAgent(task, chatId);
1025
+ return `Sub-agent spawned! 🤖\nID: ${agentId}\nTask: ${task.slice(0, 100)}${task.length > 100 ? "..." : ""}\n\nThe agent is working in the background. Results will be sent to this chat when complete.`;
1026
+ }
1027
+ case "list_agents": {
1028
+ const chatId = getCurrentChatId();
1029
+ const agents = listAgents(chatId || undefined);
1030
+ if (agents.length === 0) {
1031
+ return "No sub-agents found.";
1032
+ }
1033
+ const lines = agents.map((a) => {
1034
+ const status = {
1035
+ running: "🔄 Running",
1036
+ completed: "✅ Completed",
1037
+ failed: "❌ Failed",
1038
+ cancelled: "⏹️ Cancelled",
1039
+ }[a.status];
1040
+ const time = a.completedAt
1041
+ ? `(${Math.round((a.completedAt.getTime() - a.createdAt.getTime()) / 1000)}s)`
1042
+ : "";
1043
+ return `${a.id}: ${status} ${time}\n Task: ${a.task.slice(0, 60)}${a.task.length > 60 ? "..." : ""}`;
1044
+ });
1045
+ return `Sub-agents:\n${lines.join("\n\n")}`;
1046
+ }
1047
+ case "cancel_agent": {
1048
+ const agentId = input.agent_id;
1049
+ if (!agentId) {
1050
+ return "Error: Agent ID is required";
1051
+ }
1052
+ const success = cancelAgent(agentId);
1053
+ if (success) {
1054
+ return `Sub-agent ${agentId} cancelled.`;
1055
+ }
1056
+ else {
1057
+ return `Could not cancel agent ${agentId}. It may not exist or already completed.`;
1058
+ }
1059
+ }
753
1060
  default:
754
1061
  return `Error: Unknown tool: ${name}`;
755
1062
  }
@@ -772,6 +1079,10 @@ export function getToolsDescription(modelId) {
772
1079
 
773
1080
  ## 시스템
774
1081
  - run_command: 셸 명령어 실행 (git, npm 등)
1082
+ - background=true: 백그라운드 실행, 세션 ID 반환
1083
+ - list_sessions: 백그라운드 세션 목록
1084
+ - get_session_log: 세션 출력 로그 조회
1085
+ - kill_session: 세션 종료
775
1086
  - change_model: AI 모델 변경
776
1087
  - sonnet: 범용 (기본)
777
1088
  - opus: 복잡한 작업
@@ -804,5 +1115,10 @@ export function getToolsDescription(modelId) {
804
1115
  ## 온보딩
805
1116
  - save_persona: 페르소나 설정 저장 (온보딩 완료 시)
806
1117
 
1118
+ ## Sub-Agent (백그라운드 작업)
1119
+ - spawn_agent: 복잡한 작업을 sub-agent에게 위임 (독립 실행)
1120
+ - list_agents: 활성 sub-agent 목록
1121
+ - cancel_agent: sub-agent 취소
1122
+
807
1123
  허용된 경로: ${path.join(home, "Documents")}, ${path.join(home, "projects")}, 워크스페이스`;
808
1124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "companionbot",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "AI 친구 텔레그램 봇 - Claude API 기반 개인화된 대화 상대",
5
5
  "keywords": [
6
6
  "telegram",