claude-telegram-bot 0.3.1 → 0.3.3

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.
Files changed (3) hide show
  1. package/bot.mjs +55 -3
  2. package/ctb.mjs +14 -4
  3. package/package.json +1 -1
package/bot.mjs CHANGED
@@ -83,6 +83,7 @@ const stateFile = stateBase === "config" ? "state.json" : `${stateBase}.state.js
83
83
  const BOT_DIR = join(DATA_DIR, ".claude-bot");
84
84
  const STATE_PATH = join(BOT_DIR, stateFile);
85
85
  const ATTACH_DIR = join(BOT_DIR, "attachments");
86
+ const MEMORY_PATH = join(BOT_DIR, "memory.md"); // /new 로 초기화해도 유지되는 퍼시스턴트 메모리
86
87
  const LEGACY_STATE_PATH = join(DATA_DIR, stateFile); // 구버전(루트 직하) 호환
87
88
  const LEGACY_ATTACH_DIR = join(DATA_DIR, "attachments");
88
89
 
@@ -130,6 +131,8 @@ const STR = {
130
131
  "• /new — reset conversation context (new session)\n" +
131
132
  "• /stop — stop the current task · /stop --reset to also roll back the session\n" +
132
133
  "• /cron — list tasks · /cron add <natural language> to add · /cron rm <id> to remove\n" +
134
+ "• /remember <text> — save to persistent memory (survives /new)\n" +
135
+ "• /memory — view memory · /memory clear to wipe\n" +
133
136
  "• /restart — restart the bot (after a syntax check)\n" +
134
137
  "• /status — bot status & version\n" +
135
138
  "• /model — view / switch the model\n" +
@@ -183,6 +186,12 @@ const STR = {
183
186
  `/model default — clear the override`,
184
187
  modelSet: (m) => `🧠 Model set to: ${m}`,
185
188
  modelReset: (def) => `🧠 Model reset to default (${def}).`,
189
+ memoryEmpty: "No memory yet. Use `/remember <text>` to add.",
190
+ memoryShow: (m) => `💾 Memory:\n\`\`\`\n${m}\n\`\`\``,
191
+ memoryCleared: "🧹 Memory cleared.",
192
+ remembered: "💾 Saved to memory.",
193
+ rememberUsage: "Usage: /remember <text to remember>",
194
+ memoryUsage: "Usage: /memory · /memory clear",
186
195
  },
187
196
  ko: {
188
197
  help: () =>
@@ -191,6 +200,8 @@ const STR = {
191
200
  "• /new — 대화 맥락 초기화 (새 세션)\n" +
192
201
  "• /stop — 진행 중인 작업 중단 · /stop --reset 으로 세션도 되돌리기\n" +
193
202
  "• /cron — 예약 작업 보기 · /cron add <자연어>로 추가 · /cron rm <번호>로 삭제\n" +
203
+ "• /remember <내용> — 퍼시스턴트 메모리에 저장 (/new 로 초기화해도 유지)\n" +
204
+ "• /memory — 메모리 보기 · /memory clear 로 삭제\n" +
194
205
  "• /restart — 봇 재시작 (문법 검사 후 안전하게)\n" +
195
206
  "• /status — 봇 상태·버전 보기\n" +
196
207
  "• /model — 모델 보기·전환\n" +
@@ -243,6 +254,12 @@ const STR = {
243
254
  `/model default — 오버라이드 해제`,
244
255
  modelSet: (m) => `🧠 모델을 ${m} (으)로 설정했습니다.`,
245
256
  modelReset: (def) => `🧠 모델을 기본값(${def})으로 되돌렸습니다.`,
257
+ memoryEmpty: "저장된 메모리가 없습니다. `/remember <내용>`으로 추가하세요.",
258
+ memoryShow: (m) => `💾 메모리:\n\`\`\`\n${m}\n\`\`\``,
259
+ memoryCleared: "🧹 메모리를 삭제했습니다.",
260
+ remembered: "💾 메모리에 저장했습니다.",
261
+ rememberUsage: "사용법: /remember <기억할 내용>",
262
+ memoryUsage: "사용법: /memory · /memory clear",
246
263
  },
247
264
  };
248
265
  const t = (l, key, ...a) => {
@@ -258,6 +275,8 @@ const COMMANDS = {
258
275
  en: [
259
276
  { command: "new", description: "Reset context (new session)" },
260
277
  { command: "stop", description: "Stop the current task (--reset to roll back session)" },
278
+ { command: "remember", description: "Save to persistent memory (survives /new)" },
279
+ { command: "memory", description: "View or clear persistent memory" },
261
280
  { command: "cron", description: "List / add / remove scheduled tasks" },
262
281
  { command: "restart", description: "Restart the bot (after syntax check)" },
263
282
  { command: "status", description: "Bot status / version" },
@@ -268,6 +287,8 @@ const COMMANDS = {
268
287
  ko: [
269
288
  { command: "new", description: "대화 맥락 초기화 (새 세션)" },
270
289
  { command: "stop", description: "작업 중단 (--reset 으로 세션 되돌리기)" },
290
+ { command: "remember", description: "퍼시스턴트 메모리에 저장 (/new 후에도 유지)" },
291
+ { command: "memory", description: "메모리 보기·삭제" },
271
292
  { command: "cron", description: "예약 작업 보기·추가·삭제" },
272
293
  { command: "restart", description: "봇 재시작 (문법 검사 후)" },
273
294
  { command: "status", description: "봇 상태·버전 보기" },
@@ -294,6 +315,15 @@ function checkLocalLock() {
294
315
  }
295
316
  }
296
317
 
318
+ // ── 퍼시스턴트 메모리 ─────────────────────────────────────────────────────
319
+ // /new 로 세션을 초기화해도 유지되는 메모리. runClaude 시 시스템 프롬프트에 주입.
320
+ function loadMemory() {
321
+ try { return readFileSync(MEMORY_PATH, "utf8").trim(); } catch { return ""; }
322
+ }
323
+ function saveMemory(content) {
324
+ writeFileSync(MEMORY_PATH, content);
325
+ }
326
+
297
327
  // ── 상태 (세션 이어가기용) ────────────────────────────────────────────────
298
328
  function loadState() {
299
329
  // 새 경로(.claude-bot/) 우선, 없으면 구버전 루트 경로로 폴백(이주 실패 시 안전망).
@@ -430,8 +460,11 @@ function runClaude(prompt, sessionId, opts = {}) {
430
460
  const modelHint = opts.modelHint
431
461
  ? `Current model: ${model || "claude (default)"}. Model tiers (low→high): haiku → sonnet → opus → fable. If this question seems to require more capability than the current model, append one short line at the very end of your reply: 💡 \`/model sonnet\` (or \`/model opus\`, \`/model fable\`) for a stronger answer. Omit the suggestion for simple questions.`
432
462
  : null;
433
- // 페르소나(cfg.persona) + 간결 지침 + 모델 힌트를 함께 주입 → 멀티 봇(역할별) 운영용
434
- const appendSys = [cfg.persona, brevity, modelHint].filter(Boolean).join("\n\n");
463
+ // opts.injectMemory: 퍼시스턴트 메모리를 시스템 프롬프트에 주입 (/new 초기화해도 유지)
464
+ const mem = opts.injectMemory ? loadMemory() : "";
465
+ const memoryBlock = mem ? `## Persistent memory (survives /new)\n${mem}` : null;
466
+ // 페르소나(cfg.persona) + 간결 지침 + 모델 힌트 + 메모리를 함께 주입
467
+ const appendSys = [cfg.persona, brevity, modelHint, memoryBlock].filter(Boolean).join("\n\n");
435
468
  if (appendSys) args.push("--append-system-prompt", appendSys);
436
469
  if (model) args.push("--model", model);
437
470
  if (sessionId) args.push("--resume", sessionId);
@@ -821,6 +854,25 @@ async function handle(msg) {
821
854
  await send(chatId, t(l, reset ? "stopReset" : "stopOk"));
822
855
  return;
823
856
  }
857
+ if (text.startsWith("/remember ")) {
858
+ const content = text.slice(10).trim();
859
+ if (!content) { await send(chatId, t(l, "rememberUsage")); return; }
860
+ const existing = loadMemory();
861
+ saveMemory(existing ? `${existing}\n- ${content}` : `- ${content}`);
862
+ await send(chatId, t(l, "remembered"));
863
+ return;
864
+ }
865
+ if (text === "/memory" || text.startsWith("/memory ")) {
866
+ const arg = text.slice(7).trim();
867
+ if (arg === "clear") {
868
+ saveMemory("");
869
+ await send(chatId, t(l, "memoryCleared"));
870
+ return;
871
+ }
872
+ const mem = loadMemory();
873
+ await send(chatId, mem ? t(l, "memoryShow", mem) : t(l, "memoryEmpty"));
874
+ return;
875
+ }
824
876
 
825
877
  if (busy) {
826
878
  msgQueue.push({ msg, receivedAt: Date.now() });
@@ -855,7 +907,7 @@ async function handle(msg) {
855
907
  }
856
908
  }
857
909
  prevSessionId = state.sessionId; // /stop --reset 복원 대상 저장
858
- const res = await runClaude(prompt, state.sessionId, { modelHint: true, trackChild: true });
910
+ const res = await runClaude(prompt, state.sessionId, { modelHint: true, trackChild: true, injectMemory: true });
859
911
  if (res.sessionId) {
860
912
  state.sessionId = res.sessionId;
861
913
  saveState(state);
package/ctb.mjs CHANGED
@@ -13,7 +13,7 @@
13
13
  // While Claude runs, .claude-bot/local.lock (PID) is created so the bot defers
14
14
  // incoming Telegram messages until the local session ends.
15
15
 
16
- import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
16
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
17
17
  import { basename, dirname, join } from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { spawn } from "node:child_process";
@@ -38,12 +38,22 @@ function runBot(botArgs) {
38
38
  }
39
39
 
40
40
  function resolveConfig(arg) {
41
- if (!arg) return process.env.BOT_CONFIG || join(HERE, "config.json");
41
+ if (!arg) {
42
+ if (process.env.BOT_CONFIG) return process.env.BOT_CONFIG;
43
+ // cwd 우선(프로젝트 폴더에서 ctb 실행) → 전역 설치 경로 폴백
44
+ for (const base of [process.cwd(), HERE]) {
45
+ for (const name of ["mybot.json", "config.json"]) {
46
+ const p = join(base, name);
47
+ if (existsSync(p)) return p;
48
+ }
49
+ }
50
+ return join(process.cwd(), "mybot.json"); // 최종 폴백
51
+ }
42
52
  // Absolute or explicitly relative path → use as-is
43
53
  if (arg.startsWith("/") || arg.startsWith("./") || arg.startsWith("../"))
44
54
  return arg;
45
- // Bare name (e.g. "planner.json") → relative to package dir
46
- return join(HERE, arg);
55
+ // Bare name (e.g. "planner.json") → relative to cwd first, then package dir
56
+ return existsSync(join(process.cwd(), arg)) ? join(process.cwd(), arg) : join(HERE, arg);
47
57
  }
48
58
 
49
59
  function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-telegram-bot",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Drive Claude Code from Telegram — messages run headless `claude -p` in a project dir and replies come back to the chat. Zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {