@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.
- package/bot.js +147 -24
- 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
|
-
|
|
510
|
-
|
|
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]
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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; }
|