@inetafrica/open-claudia 1.0.9 → 1.1.1

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 (2) hide show
  1. package/bot.js +147 -24
  2. package/package.json +1 -1
package/bot.js CHANGED
@@ -70,6 +70,7 @@ bot.setMyCommands([
70
70
  { command: "effort", description: "Set effort level" },
71
71
  { command: "budget", description: "Set max spend for next task" },
72
72
  { command: "plan", description: "Toggle plan mode" },
73
+ { command: "sessions", description: "List conversations for this project" },
73
74
  { command: "compact", description: "Summarize conversation context" },
74
75
  { command: "continue", description: "Resume last conversation" },
75
76
  { command: "worktree", description: "Toggle isolated git branch" },
@@ -88,19 +89,83 @@ bot.setMyCommands([
88
89
  const TEMP_DIR = path.join(WORKSPACE, ".telegram-media");
89
90
  if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
90
91
 
92
+ // ── Persistent state ───────────────────────────────────────────────
93
+ const STATE_FILE = path.join(CONFIG_DIR, "state.json");
94
+ const SESSIONS_FILE = path.join(CONFIG_DIR, "sessions.json");
95
+
96
+ function loadState() {
97
+ try {
98
+ return JSON.parse(fs.readFileSync(STATE_FILE, "utf-8"));
99
+ } catch (e) {
100
+ return {};
101
+ }
102
+ }
103
+
104
+ function saveState() {
105
+ const data = {
106
+ currentSession,
107
+ lastSessionId,
108
+ settings,
109
+ };
110
+ try { fs.writeFileSync(STATE_FILE, JSON.stringify(data)); } catch (e) {}
111
+ }
112
+
113
+ // ── Per-project session history ────────────────────────────────────
114
+
115
+ function loadSessions() {
116
+ try { return JSON.parse(fs.readFileSync(SESSIONS_FILE, "utf-8")); } catch (e) { return {}; }
117
+ }
118
+
119
+ function saveSessions(sessions) {
120
+ try { fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions, null, 2)); } catch (e) {}
121
+ }
122
+
123
+ function recordSession(projectName, sessionId, title) {
124
+ const sessions = loadSessions();
125
+ if (!sessions[projectName]) sessions[projectName] = [];
126
+ const existing = sessions[projectName].find((s) => s.id === sessionId);
127
+ if (existing) {
128
+ if (title) existing.title = title;
129
+ existing.lastUsed = new Date().toISOString();
130
+ } else {
131
+ sessions[projectName].push({
132
+ id: sessionId,
133
+ title: title || "Untitled",
134
+ created: new Date().toISOString(),
135
+ lastUsed: new Date().toISOString(),
136
+ });
137
+ }
138
+ // Keep last 20 sessions per project
139
+ sessions[projectName] = sessions[projectName].slice(-20);
140
+ saveSessions(sessions);
141
+ }
142
+
143
+ function getProjectSessions(projectName) {
144
+ const sessions = loadSessions();
145
+ return (sessions[projectName] || []).slice().reverse();
146
+ }
147
+
148
+ function getLastProjectSession(projectName) {
149
+ const sessions = getProjectSessions(projectName);
150
+ return sessions.length > 0 ? sessions[0] : null;
151
+ }
152
+
153
+ const savedState = loadState();
154
+
91
155
  // ── State ───────────────────────────────────────────────────────────
92
- let currentSession = null;
156
+ let currentSession = savedState.currentSession || null;
93
157
  let runningProcess = null;
94
158
  let statusMessageId = null;
95
159
  let streamBuffer = "";
96
160
  let streamInterval = null;
97
- let lastSessionId = null;
161
+ let lastSessionId = savedState.lastSessionId || null;
98
162
  let messageQueue = [];
99
163
  let activeCrons = new Map();
100
164
  let pendingVaultUnlock = false; // Waiting for password
101
165
  let pendingVaultAction = null; // What to do after unlock
166
+ let isFirstMessage = !lastSessionId; // Track if this is first message in session
102
167
 
103
- let settings = {
168
+ let settings = savedState.settings || {
104
169
  model: null, effort: null, budget: null, permissionMode: null, worktree: false,
105
170
  };
106
171
 
@@ -494,7 +559,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
494
559
  else if (block.type === "tool_use") { currentTool = block.name; toolUses.push(block.name); }
495
560
  }
496
561
  }
497
- if (evt.type === "result" && evt.session_id) lastSessionId = evt.session_id;
562
+ if (evt.type === "result" && evt.session_id) { lastSessionId = evt.session_id; saveState(); }
498
563
  if (evt.type === "result" && evt.result) assistantText = evt.result;
499
564
  }
500
565
  });
@@ -506,18 +571,21 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
506
571
  clearInterval(streamInterval); streamInterval = null;
507
572
  const finalText = assistantText || "(no output)";
508
573
  const chunks = splitMessage(finalText);
509
- const btns = { inline_keyboard: [[
510
- { text: "Continue", callback_data: "a:continue" },
511
- { text: "End session", callback_data: "a:end" },
512
- ]] };
513
- if (statusMessageId) await editMessage(statusMessageId, chunks[0], { keyboard: btns });
514
- else await send(chunks[0], { keyboard: btns, replyTo: replyToMsgId });
574
+ if (statusMessageId) await editMessage(statusMessageId, chunks[0]);
575
+ else await send(chunks[0], { replyTo: replyToMsgId });
515
576
  for (let i = 1; i < chunks.length; i++) {
516
- await send(chunks[i], i === chunks.length - 1 ? { keyboard: btns } : {});
577
+ await send(chunks[i]);
517
578
  }
518
579
  if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
519
580
  if (settings.budget) settings.budget = null;
520
581
  statusMessageId = null;
582
+
583
+ // Record session with auto-title from first message
584
+ if (lastSessionId && currentSession) {
585
+ const title = isFirstMessage ? (prompt.length > 60 ? prompt.slice(0, 57) + "..." : prompt) : null;
586
+ recordSession(currentSession.name, lastSessionId, title);
587
+ isFirstMessage = false;
588
+ }
521
589
  if (messageQueue.length > 0 && currentSession) {
522
590
  const next = messageQueue.shift();
523
591
  await runClaude(next.prompt, currentSession.dir, next.replyToMsgId, next.opts);
@@ -575,19 +643,47 @@ function initCrons() {
575
643
 
576
644
  // ── Session ─────────────────────────────────────────────────────────
577
645
 
578
- function startSession(name) {
646
+ function startSession(name, resumeSessionId) {
647
+ let projectName, projectDir;
579
648
  if (name === "__workspace__") {
580
- currentSession = { name: "Workspace", dir: WORKSPACE };
581
- lastSessionId = null; messageQueue = []; resetSettings();
582
- send(`Session: Workspace (root)\n\nSend text, voice, or images.`);
583
- return;
649
+ projectName = "Workspace";
650
+ projectDir = WORKSPACE;
651
+ } else {
652
+ const result = findProject(name);
653
+ if (!result) return send(`No match for "${name}".`, { keyboard: projectKeyboard() });
654
+ if (Array.isArray(result)) return send("Multiple matches:", { keyboard: { inline_keyboard: result.map((p) => [{ text: p, callback_data: `s:${p}` }]) } });
655
+ projectName = result;
656
+ projectDir = path.join(WORKSPACE, result);
657
+ }
658
+
659
+ currentSession = { name: projectName, dir: projectDir };
660
+ messageQueue = []; resetSettings();
661
+
662
+ // Resume a specific session or the last one for this project
663
+ if (resumeSessionId) {
664
+ lastSessionId = resumeSessionId;
665
+ const sessions = getProjectSessions(projectName);
666
+ const s = sessions.find((x) => x.id === resumeSessionId);
667
+ const title = s ? s.title : "";
668
+ isFirstMessage = false;
669
+ saveState();
670
+ send(`Session: ${projectName}\nResumed: ${title || resumeSessionId.slice(0, 8)}\n\nSend text, voice, or images.`);
671
+ } else {
672
+ const last = getLastProjectSession(projectName);
673
+ if (last) {
674
+ lastSessionId = last.id;
675
+ isFirstMessage = false;
676
+ saveState();
677
+ send(`Session: ${projectName}\nResumed: ${last.title}\n\nSend text, voice, or images.`, {
678
+ keyboard: { inline_keyboard: [[{ text: "New conversation", callback_data: `new:${projectName}` }]] },
679
+ });
680
+ } else {
681
+ lastSessionId = null;
682
+ isFirstMessage = true;
683
+ saveState();
684
+ send(`Session: ${projectName}\n\nSend text, voice, or images.`);
685
+ }
584
686
  }
585
- const result = findProject(name);
586
- if (!result) return send(`No match for "${name}".`, { keyboard: projectKeyboard() });
587
- if (Array.isArray(result)) return send("Multiple matches:", { keyboard: { inline_keyboard: result.map((p) => [{ text: p, callback_data: `s:${p}` }]) } });
588
- currentSession = { name: result, dir: path.join(WORKSPACE, result) };
589
- lastSessionId = null; messageQueue = []; resetSettings();
590
- send(`Session: ${result}\n\nSend text, voice, or images.`);
591
687
  }
592
688
 
593
689
  function requireSession(msg) {
@@ -606,7 +702,7 @@ bot.onText(/\/start/, (msg) => {
606
702
  bot.onText(/\/help/, (msg) => {
607
703
  if (!isAuthorized(msg)) return;
608
704
  send([
609
- "Session: /session /projects /continue /status /stop /end",
705
+ "Session: /session /sessions /projects /continue /status /stop /end",
610
706
  "Settings: /model /effort /budget /plan /compact /worktree",
611
707
  "Automation: /cron /vault /soul",
612
708
  "System: /restart /upgrade",
@@ -669,6 +765,20 @@ bot.onText(/\/session$/, (msg) => {
669
765
  });
670
766
  bot.onText(/\/session (.+)/, (msg, match) => { if (isAuthorized(msg)) startSession(match[1].trim()); });
671
767
 
768
+ bot.onText(/\/sessions$/, (msg) => {
769
+ if (!isAuthorized(msg)) return;
770
+ if (!requireSession(msg)) return;
771
+ const sessions = getProjectSessions(currentSession.name);
772
+ if (sessions.length === 0) return send("No past conversations for this project.");
773
+ const rows = sessions.slice(0, 10).map((s) => {
774
+ const date = new Date(s.lastUsed).toLocaleDateString();
775
+ const active = lastSessionId === s.id ? " (active)" : "";
776
+ return [{ text: `${s.title}${active} — ${date}`, callback_data: `ss:${s.id}` }];
777
+ });
778
+ rows.push([{ text: "New conversation", callback_data: `new:${currentSession.name}` }]);
779
+ send(`Conversations in ${currentSession.name}:`, { keyboard: { inline_keyboard: rows } });
780
+ });
781
+
672
782
  bot.onText(/\/model$/, (msg) => {
673
783
  if (!isAuthorized(msg)) return;
674
784
  send(`Model: ${settings.model || "default"}`, { keyboard: { inline_keyboard: [
@@ -722,6 +832,7 @@ bot.onText(/\/end/, (msg) => {
722
832
  if (!isAuthorized(msg)) return;
723
833
  if (currentSession) {
724
834
  const n = currentSession.name; currentSession = null; lastSessionId = null; messageQueue = []; resetSettings();
835
+ saveState();
725
836
  send(`Ended: ${n}`, { keyboard: { inline_keyboard: [[{ text: "New session", callback_data: "show:projects" }]] } });
726
837
  } else send("No session.");
727
838
  });
@@ -833,8 +944,20 @@ bot.on("callback_query", async (q) => {
833
944
 
834
945
  if (d === "show:projects") { await send("Pick:", { keyboard: projectKeyboard() }); return; }
835
946
  if (d.startsWith("s:")) { startSession(d.slice(2)); return; }
947
+ if (d.startsWith("ss:")) { if (currentSession) startSession(currentSession.name, d.slice(3)); return; }
948
+ if (d.startsWith("new:")) {
949
+ const proj = d.slice(4);
950
+ // Clear session history so startSession doesn't auto-resume
951
+ const sessions = loadSessions();
952
+ // Don't delete history, just start fresh
953
+ currentSession = { name: proj === "__workspace__" ? "Workspace" : proj, dir: proj === "__workspace__" ? WORKSPACE : path.join(WORKSPACE, proj) };
954
+ lastSessionId = null; isFirstMessage = true; messageQueue = []; resetSettings();
955
+ saveState();
956
+ await send(`Session: ${currentSession.name}\nNew conversation\n\nSend text, voice, or images.`);
957
+ return;
958
+ }
836
959
  if (d === "a:continue") { if (currentSession) await runClaude("continue", currentSession.dir); else send("No session."); return; }
837
- if (d === "a:end") { if (currentSession) { const n = currentSession.name; currentSession = null; lastSessionId = null; messageQueue = []; resetSettings(); await send(`Ended: ${n}`, { keyboard: { inline_keyboard: [[{ text: "New", callback_data: "show:projects" }]] } }); } return; }
960
+ if (d === "a:end") { if (currentSession) { const n = currentSession.name; currentSession = null; lastSessionId = null; messageQueue = []; resetSettings(); saveState(); await send(`Ended: ${n}`, { keyboard: { inline_keyboard: [[{ text: "New", callback_data: "show:projects" }]] } }); } return; }
838
961
  if (d.startsWith("m:")) { settings.model = d.slice(2) === "default" ? null : d.slice(2); await send(`Model: ${settings.model || "default"}`); return; }
839
962
  if (d.startsWith("e:")) { const e = d.slice(2); settings.effort = e === "default" ? null : e; await send(`Effort: ${settings.effort || "default"}`); return; }
840
963
  if (d.startsWith("b:")) { const b = d.slice(2); settings.budget = b === "none" ? null : parseFloat(b); await send(`Budget: ${settings.budget ? "$"+settings.budget : "none"}`); return; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "1.0.9",
3
+ "version": "1.1.1",
4
4
  "description": "Your always-on AI coding assistant — Claude Code via Telegram",
5
5
  "main": "bot.js",
6
6
  "bin": {