@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.
- package/bot.js +118 -20
- 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
|
-
|
|
532
|
-
|
|
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]
|
|
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
|
-
|
|
603
|
-
|
|
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:
|
|
606
|
-
|
|
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; }
|