@inetafrica/open-claudia 1.1.0 → 1.1.2

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 +118 -20
  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" },
@@ -90,6 +91,7 @@ if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
90
91
 
91
92
  // ── Persistent state ───────────────────────────────────────────────
92
93
  const STATE_FILE = path.join(CONFIG_DIR, "state.json");
94
+ const SESSIONS_FILE = path.join(CONFIG_DIR, "sessions.json");
93
95
 
94
96
  function loadState() {
95
97
  try {
@@ -108,6 +110,46 @@ function saveState() {
108
110
  try { fs.writeFileSync(STATE_FILE, JSON.stringify(data)); } catch (e) {}
109
111
  }
110
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
+
111
153
  const savedState = loadState();
112
154
 
113
155
  // ── State ───────────────────────────────────────────────────────────
@@ -121,6 +163,7 @@ let messageQueue = [];
121
163
  let activeCrons = new Map();
122
164
  let pendingVaultUnlock = false; // Waiting for password
123
165
  let pendingVaultAction = null; // What to do after unlock
166
+ let isFirstMessage = !lastSessionId; // Track if this is first message in session
124
167
 
125
168
  let settings = savedState.settings || {
126
169
  model: null, effort: null, budget: null, permissionMode: null, worktree: false,
@@ -528,18 +571,21 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
528
571
  clearInterval(streamInterval); streamInterval = null;
529
572
  const finalText = assistantText || "(no output)";
530
573
  const chunks = splitMessage(finalText);
531
- const btns = { inline_keyboard: [[
532
- { text: "Continue", callback_data: "a:continue" },
533
- { text: "End session", callback_data: "a:end" },
534
- ]] };
535
- if (statusMessageId) await editMessage(statusMessageId, chunks[0], { keyboard: btns });
536
- 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 });
537
576
  for (let i = 1; i < chunks.length; i++) {
538
- await send(chunks[i], i === chunks.length - 1 ? { keyboard: btns } : {});
577
+ await send(chunks[i]);
539
578
  }
540
579
  if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
541
580
  if (settings.budget) settings.budget = null;
542
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
+ }
543
589
  if (messageQueue.length > 0 && currentSession) {
544
590
  const next = messageQueue.shift();
545
591
  await runClaude(next.prompt, currentSession.dir, next.replyToMsgId, next.opts);
@@ -597,21 +643,47 @@ function initCrons() {
597
643
 
598
644
  // ── Session ─────────────────────────────────────────────────────────
599
645
 
600
- function startSession(name) {
646
+ function startSession(name, resumeSessionId) {
647
+ let projectName, projectDir;
601
648
  if (name === "__workspace__") {
602
- currentSession = { name: "Workspace", dir: WORKSPACE };
603
- lastSessionId = null; messageQueue = []; resetSettings();
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;
604
669
  saveState();
605
- send(`Session: Workspace (root)\n\nSend text, voice, or images.`);
606
- return;
670
+ send(`Session: ${projectName}\nResumed: ${title || resumeSessionId.slice(0, 8)}\n\nSend text, voice, or images.\n\n/sessions — switch conversation\n/session — switch project`);
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.\n\n/sessions — switch conversation\n/session — switch project`, {
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.\n\n/sessions — switch conversation\n/session — switch project`);
685
+ }
607
686
  }
608
- const result = findProject(name);
609
- if (!result) return send(`No match for "${name}".`, { keyboard: projectKeyboard() });
610
- if (Array.isArray(result)) return send("Multiple matches:", { keyboard: { inline_keyboard: result.map((p) => [{ text: p, callback_data: `s:${p}` }]) } });
611
- currentSession = { name: result, dir: path.join(WORKSPACE, result) };
612
- lastSessionId = null; messageQueue = []; resetSettings();
613
- saveState();
614
- send(`Session: ${result}\n\nSend text, voice, or images.`);
615
687
  }
616
688
 
617
689
  function requireSession(msg) {
@@ -630,7 +702,7 @@ bot.onText(/\/start/, (msg) => {
630
702
  bot.onText(/\/help/, (msg) => {
631
703
  if (!isAuthorized(msg)) return;
632
704
  send([
633
- "Session: /session /projects /continue /status /stop /end",
705
+ "Session: /session /sessions /projects /continue /status /stop /end",
634
706
  "Settings: /model /effort /budget /plan /compact /worktree",
635
707
  "Automation: /cron /vault /soul",
636
708
  "System: /restart /upgrade",
@@ -693,6 +765,20 @@ bot.onText(/\/session$/, (msg) => {
693
765
  });
694
766
  bot.onText(/\/session (.+)/, (msg, match) => { if (isAuthorized(msg)) startSession(match[1].trim()); });
695
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
+
696
782
  bot.onText(/\/model$/, (msg) => {
697
783
  if (!isAuthorized(msg)) return;
698
784
  send(`Model: ${settings.model || "default"}`, { keyboard: { inline_keyboard: [
@@ -858,6 +944,18 @@ bot.on("callback_query", async (q) => {
858
944
 
859
945
  if (d === "show:projects") { await send("Pick:", { keyboard: projectKeyboard() }); return; }
860
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.\n\n/sessions — switch conversation\n/session — switch project`);
957
+ return;
958
+ }
861
959
  if (d === "a:continue") { if (currentSession) await runClaude("continue", currentSession.dir); else send("No session."); return; }
862
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; }
863
961
  if (d.startsWith("m:")) { settings.model = d.slice(2) === "default" ? null : d.slice(2); await send(`Model: ${settings.model || "default"}`); return; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Your always-on AI coding assistant — Claude Code via Telegram",
5
5
  "main": "bot.js",
6
6
  "bin": {