@inetafrica/open-claudia 1.6.0 → 1.7.0
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/Dockerfile +5 -0
- package/bin/cli.js +21 -7
- package/bot-agent.js +1487 -0
- package/bot.js +48 -197
- package/docker-entrypoint.sh +47 -0
- package/package.json +3 -1
- package/k8s/deployment.yaml +0 -61
- package/k8s/ingress.yaml +0 -26
- package/k8s/pvc.yaml +0 -12
- package/k8s/secret.yaml +0 -10
- package/k8s/service.yaml +0 -14
package/bot.js
CHANGED
|
@@ -170,7 +170,6 @@ bot.setMyCommands([
|
|
|
170
170
|
{ command: "compact", description: "Summarize conversation context" },
|
|
171
171
|
{ command: "continue", description: "Resume last conversation" },
|
|
172
172
|
{ command: "worktree", description: "Toggle isolated git branch" },
|
|
173
|
-
{ command: "agents", description: "Toggle direct/parallel agent mode" },
|
|
174
173
|
{ command: "cron", description: "Manage scheduled tasks" },
|
|
175
174
|
{ command: "vault", description: "Manage credentials (password required)" },
|
|
176
175
|
{ command: "soul", description: "View/edit assistant identity" },
|
|
@@ -268,8 +267,7 @@ const savedState = loadState();
|
|
|
268
267
|
|
|
269
268
|
// ── State ───────────────────────────────────────────────────────────
|
|
270
269
|
let currentSession = savedState.currentSession || null;
|
|
271
|
-
let runningProcess = null;
|
|
272
|
-
let runningTasks = new Map(); // taskId -> { proc, statusMessageId, streamInterval, startTime } (parallel mode)
|
|
270
|
+
let runningProcess = null;
|
|
273
271
|
let statusMessageId = null;
|
|
274
272
|
let streamBuffer = "";
|
|
275
273
|
let streamInterval = null;
|
|
@@ -279,14 +277,13 @@ let activeCrons = new Map();
|
|
|
279
277
|
let pendingVaultUnlock = false; // Waiting for password
|
|
280
278
|
let pendingVaultAction = null; // What to do after unlock
|
|
281
279
|
let isFirstMessage = !lastSessionId; // Track if this is first message in session
|
|
282
|
-
let taskCounter = 0;
|
|
283
280
|
|
|
284
281
|
let settings = savedState.settings || {
|
|
285
|
-
model: null, effort: null, budget: null, permissionMode: null, worktree: false,
|
|
282
|
+
model: null, effort: null, budget: null, permissionMode: null, worktree: false,
|
|
286
283
|
};
|
|
287
284
|
|
|
288
285
|
function resetSettings() {
|
|
289
|
-
settings = { model: null, effort: null, budget: null, permissionMode: null, worktree: false
|
|
286
|
+
settings = { model: null, effort: null, budget: null, permissionMode: null, worktree: false };
|
|
290
287
|
}
|
|
291
288
|
|
|
292
289
|
function isAuthorized(msg) {
|
|
@@ -656,19 +653,6 @@ function parseStreamEvents(data) {
|
|
|
656
653
|
return events;
|
|
657
654
|
}
|
|
658
655
|
|
|
659
|
-
/**
|
|
660
|
-
* Route to the appropriate runner based on agent mode setting.
|
|
661
|
-
* In direct mode: serial execution with shared session.
|
|
662
|
-
* In parallel mode: concurrent independent processes.
|
|
663
|
-
* Commands like /compact and /continue always use direct mode (they need session context).
|
|
664
|
-
*/
|
|
665
|
-
async function runClaudeRouted(prompt, cwd, replyToMsgId, opts = {}) {
|
|
666
|
-
if (opts.forceDirect || settings.agentMode !== "parallel") {
|
|
667
|
-
return runClaude(prompt, cwd, replyToMsgId, opts);
|
|
668
|
-
}
|
|
669
|
-
return runClaudeParallel(prompt, cwd, replyToMsgId, opts);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
656
|
function buildClaudeArgs(prompt, opts = {}) {
|
|
673
657
|
const args = ["-p", "--verbose", "--output-format", "stream-json",
|
|
674
658
|
"--append-system-prompt", buildSystemPrompt()];
|
|
@@ -780,19 +764,29 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
780
764
|
try {
|
|
781
765
|
const finalText = assistantText || "(no output)";
|
|
782
766
|
const chunks = splitMessage(finalText);
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
767
|
+
const firstChunk = chunks[0];
|
|
768
|
+
|
|
769
|
+
// If the progress message already shows the final text, just edit it clean
|
|
770
|
+
// (no need to send a duplicate). Only send a new message for long tasks
|
|
771
|
+
// or multi-chunk responses to trigger a notification.
|
|
772
|
+
const progressAlreadyHasFinal = statusMessageId && lastUpdate &&
|
|
773
|
+
firstChunk.length < 4000 && chunks.length === 1 &&
|
|
774
|
+
lastUpdate.includes(firstChunk.slice(0, 200));
|
|
775
|
+
|
|
776
|
+
if (progressAlreadyHasFinal) {
|
|
777
|
+
// Edit the progress message to show clean final text (remove tool steps header)
|
|
778
|
+
await editMessage(statusMessageId, firstChunk);
|
|
779
|
+
} else {
|
|
780
|
+
// Send final result as a new message (triggers notification)
|
|
781
|
+
const sent = await send(firstChunk, { replyTo: replyToMsgId });
|
|
782
|
+
if (!sent) await send(firstChunk);
|
|
783
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
784
|
+
await send(chunks[i]);
|
|
785
|
+
}
|
|
791
786
|
}
|
|
792
787
|
if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
|
|
793
788
|
} catch (e) {
|
|
794
789
|
console.error("Final message delivery failed:", e.message);
|
|
795
|
-
// Last-resort attempt to notify user something went wrong
|
|
796
790
|
await send("Task completed but failed to deliver the response. Send /continue to see the result.");
|
|
797
791
|
}
|
|
798
792
|
if (settings.budget) settings.budget = null;
|
|
@@ -816,126 +810,6 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
816
810
|
});
|
|
817
811
|
}
|
|
818
812
|
|
|
819
|
-
/**
|
|
820
|
-
* Run Claude in parallel mode — each call gets its own independent process.
|
|
821
|
-
* No queueing, no shared session. Multiple tasks can run simultaneously.
|
|
822
|
-
*/
|
|
823
|
-
async function runClaudeParallel(prompt, cwd, replyToMsgId, opts = {}) {
|
|
824
|
-
bot.sendChatAction(CHAT_ID, "typing").catch(() => {});
|
|
825
|
-
|
|
826
|
-
const taskId = `task-${++taskCounter}`;
|
|
827
|
-
let taskStatusMsgId = null;
|
|
828
|
-
let taskStreamBuf = "";
|
|
829
|
-
let taskText = "";
|
|
830
|
-
let taskToolUses = [];
|
|
831
|
-
let taskCurrentTool = null;
|
|
832
|
-
let taskCurrentToolDetail = "";
|
|
833
|
-
let taskSessionId = null;
|
|
834
|
-
|
|
835
|
-
// Build args — always fresh session in parallel mode (no --resume)
|
|
836
|
-
const args = ["-p", "--verbose", "--output-format", "stream-json",
|
|
837
|
-
"--append-system-prompt", buildSystemPrompt()];
|
|
838
|
-
if (settings.model) args.push("--model", settings.model);
|
|
839
|
-
if (settings.effort) args.push("--effort", settings.effort);
|
|
840
|
-
if (settings.budget) args.push("--max-budget-usd", String(settings.budget));
|
|
841
|
-
if (settings.permissionMode) args.push("--permission-mode", settings.permissionMode);
|
|
842
|
-
else args.push("--dangerously-skip-permissions");
|
|
843
|
-
if (settings.worktree) args.push("--worktree");
|
|
844
|
-
args.push(prompt);
|
|
845
|
-
|
|
846
|
-
const proc = spawn(CLAUDE_PATH, args, {
|
|
847
|
-
cwd,
|
|
848
|
-
env: { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME },
|
|
849
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
850
|
-
detached: process.platform !== "win32",
|
|
851
|
-
});
|
|
852
|
-
|
|
853
|
-
const startTime = Date.now();
|
|
854
|
-
let taskInterval = null;
|
|
855
|
-
let lastUpdate = "";
|
|
856
|
-
|
|
857
|
-
// Adaptive progress updates
|
|
858
|
-
const scheduleTaskUpdate = () => {
|
|
859
|
-
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
860
|
-
const interval = elapsed > 120 ? 5000 : 2000;
|
|
861
|
-
taskInterval = setTimeout(doTaskUpdate, interval);
|
|
862
|
-
};
|
|
863
|
-
const doTaskUpdate = async () => {
|
|
864
|
-
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
865
|
-
try {
|
|
866
|
-
bot.sendChatAction(CHAT_ID, "typing").catch(() => {});
|
|
867
|
-
const display = formatProgress(taskText, taskToolUses, taskCurrentTool, elapsed, taskCurrentToolDetail);
|
|
868
|
-
if (display && display !== lastUpdate) {
|
|
869
|
-
if (!taskStatusMsgId && taskText) {
|
|
870
|
-
taskStatusMsgId = await send(display.length > 4000 ? display.slice(-4000) : display, { replyTo: replyToMsgId });
|
|
871
|
-
} else if (taskStatusMsgId) {
|
|
872
|
-
await editMessage(taskStatusMsgId, display.length > 4000 ? display.slice(-4000) : display);
|
|
873
|
-
}
|
|
874
|
-
lastUpdate = display;
|
|
875
|
-
}
|
|
876
|
-
} catch (e) {
|
|
877
|
-
console.error(`Task ${taskId} progress error:`, e.message);
|
|
878
|
-
}
|
|
879
|
-
if (runningTasks.has(taskId)) scheduleTaskUpdate();
|
|
880
|
-
};
|
|
881
|
-
|
|
882
|
-
runningTasks.set(taskId, { proc, startTime, prompt: prompt.slice(0, 50) });
|
|
883
|
-
scheduleTaskUpdate();
|
|
884
|
-
|
|
885
|
-
proc.stdout.on("data", (data) => {
|
|
886
|
-
taskStreamBuf += data.toString();
|
|
887
|
-
const events = parseStreamEvents(taskStreamBuf);
|
|
888
|
-
const lastNewline = taskStreamBuf.lastIndexOf("\n");
|
|
889
|
-
taskStreamBuf = lastNewline >= 0 ? taskStreamBuf.slice(lastNewline + 1) : taskStreamBuf;
|
|
890
|
-
for (const evt of events) {
|
|
891
|
-
if (evt.type === "assistant" && evt.message?.content) {
|
|
892
|
-
for (const block of evt.message.content) {
|
|
893
|
-
if (block.type === "text") taskText += block.text;
|
|
894
|
-
else if (block.type === "tool_use") {
|
|
895
|
-
taskCurrentTool = block.name;
|
|
896
|
-
taskToolUses.push(block.name);
|
|
897
|
-
const input = block.input || {};
|
|
898
|
-
if (block.name === "Bash" && input.command) taskCurrentToolDetail = input.command.slice(0, 80);
|
|
899
|
-
else if ((block.name === "Read" || block.name === "Edit" || block.name === "Write") && input.file_path)
|
|
900
|
-
taskCurrentToolDetail = input.file_path.split("/").slice(-2).join("/");
|
|
901
|
-
else if (block.name === "Grep" && input.pattern) taskCurrentToolDetail = input.pattern.slice(0, 40);
|
|
902
|
-
else if (block.name === "Glob" && input.pattern) taskCurrentToolDetail = input.pattern;
|
|
903
|
-
else taskCurrentToolDetail = "";
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
if (evt.type === "result" && evt.session_id) taskSessionId = evt.session_id;
|
|
908
|
-
if (evt.type === "result" && evt.result) taskText = evt.result;
|
|
909
|
-
}
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
proc.stderr.on("data", (d) => console.error(`Task ${taskId} STDERR:`, d.toString()));
|
|
913
|
-
|
|
914
|
-
proc.on("close", async (code) => {
|
|
915
|
-
runningTasks.delete(taskId);
|
|
916
|
-
clearTimeout(taskInterval);
|
|
917
|
-
try {
|
|
918
|
-
const finalText = taskText || "(no output)";
|
|
919
|
-
const chunks = splitMessage(finalText);
|
|
920
|
-
const sent = await send(chunks[0], { replyTo: replyToMsgId });
|
|
921
|
-
if (!sent) await send(chunks[0]);
|
|
922
|
-
for (let i = 1; i < chunks.length; i++) await send(chunks[i]);
|
|
923
|
-
if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
|
|
924
|
-
} catch (e) {
|
|
925
|
-
console.error(`Task ${taskId} final delivery failed:`, e.message);
|
|
926
|
-
await send("Task completed but failed to deliver the response.");
|
|
927
|
-
}
|
|
928
|
-
});
|
|
929
|
-
|
|
930
|
-
proc.on("error", async (err) => {
|
|
931
|
-
runningTasks.delete(taskId);
|
|
932
|
-
clearTimeout(taskInterval);
|
|
933
|
-
await send(`Task error: ${err.message}`, { replyTo: replyToMsgId });
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
return taskId;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
813
|
async function runClaudeSilent(prompt, cwd, label) {
|
|
940
814
|
return new Promise((resolve) => {
|
|
941
815
|
const args = ["-p", "--output-format", "text", "--verbose",
|
|
@@ -1046,7 +920,7 @@ bot.onText(/\/help/, (msg) => {
|
|
|
1046
920
|
if (!isAuthorized(msg)) return;
|
|
1047
921
|
send([
|
|
1048
922
|
"Session: /session /sessions /projects /continue /status /stop /end",
|
|
1049
|
-
"Settings: /model /effort /budget /plan /compact /worktree /
|
|
923
|
+
"Settings: /model /effort /budget /plan /compact /worktree /mode",
|
|
1050
924
|
"Automation: /cron /vault /soul",
|
|
1051
925
|
"System: /restart /upgrade",
|
|
1052
926
|
"",
|
|
@@ -1154,45 +1028,27 @@ bot.onText(/\/compact/, async (msg) => { if (!isAuthorized(msg)) return; if (!re
|
|
|
1154
1028
|
bot.onText(/\/continue$/, async (msg) => { if (!isAuthorized(msg)) return; if (!requireSession(msg)) return; await runClaude("continue where we left off", currentSession.dir, msg.message_id, { continueSession: true }); });
|
|
1155
1029
|
bot.onText(/\/worktree$/, (msg) => { if (!isAuthorized(msg)) return; settings.worktree = !settings.worktree; send(settings.worktree ? "Worktree on." : "Worktree off."); });
|
|
1156
1030
|
|
|
1157
|
-
bot.onText(/\/
|
|
1031
|
+
bot.onText(/\/mode$/, async (msg) => {
|
|
1158
1032
|
if (!isAuthorized(msg)) return;
|
|
1159
|
-
|
|
1160
|
-
send(`Agent mode: *${mode}*\n\n` +
|
|
1161
|
-
`*direct* — one task at a time, shared session context, queues messages\n` +
|
|
1162
|
-
`*parallel* — concurrent tasks, independent sessions, no queue`, {
|
|
1033
|
+
await send("Bot mode: *direct* (default)\n\nSwitch to agent mode for non-blocking execution.\nIn agent mode, heavy tasks run in the background and you can keep chatting.", {
|
|
1163
1034
|
parseMode: "Markdown",
|
|
1164
1035
|
keyboard: { inline_keyboard: [
|
|
1165
|
-
[{ text:
|
|
1166
|
-
{ text: mode === "parallel" ? "Parallel (active)" : "Switch to Parallel", callback_data: "am:parallel" }],
|
|
1036
|
+
[{ text: "Switch to Agent Mode", callback_data: "mode:agent" }],
|
|
1167
1037
|
] },
|
|
1168
1038
|
});
|
|
1169
1039
|
});
|
|
1170
1040
|
|
|
1171
1041
|
bot.onText(/\/stop/, async (msg) => {
|
|
1172
1042
|
if (!isAuthorized(msg)) return;
|
|
1173
|
-
|
|
1174
|
-
// Stop parallel tasks
|
|
1175
|
-
if (runningTasks.size > 0) {
|
|
1176
|
-
for (const [taskId, task] of runningTasks) {
|
|
1177
|
-
try { process.kill(-task.proc.pid, "SIGTERM"); } catch (e) {
|
|
1178
|
-
try { task.proc.kill("SIGTERM"); } catch (e2) {}
|
|
1179
|
-
}
|
|
1180
|
-
setTimeout(() => { try { process.kill(-task.proc.pid, "SIGKILL"); } catch (e) {} }, 3000);
|
|
1181
|
-
}
|
|
1182
|
-
const count = runningTasks.size;
|
|
1183
|
-
runningTasks.clear();
|
|
1184
|
-
await send(`Cancelled ${count} task${count > 1 ? "s" : ""}.`);
|
|
1185
|
-
return;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// Stop direct mode process
|
|
1189
1043
|
if (runningProcess) {
|
|
1190
1044
|
const pid = runningProcess.pid;
|
|
1191
1045
|
try {
|
|
1046
|
+
// Kill entire process group to catch child processes (dev servers, etc.)
|
|
1192
1047
|
process.kill(-pid, "SIGTERM");
|
|
1193
1048
|
} catch (e) {
|
|
1194
1049
|
try { runningProcess.kill("SIGTERM"); } catch (e2) {}
|
|
1195
1050
|
}
|
|
1051
|
+
// Force kill after 3 seconds if still alive
|
|
1196
1052
|
setTimeout(() => {
|
|
1197
1053
|
try { process.kill(-pid, "SIGKILL"); } catch (e) {}
|
|
1198
1054
|
}, 3000);
|
|
@@ -1200,33 +1056,19 @@ bot.onText(/\/stop/, async (msg) => {
|
|
|
1200
1056
|
if (streamInterval) clearTimeout(streamInterval);
|
|
1201
1057
|
messageQueue = [];
|
|
1202
1058
|
await send("Cancelled.");
|
|
1203
|
-
return;
|
|
1204
1059
|
}
|
|
1205
|
-
|
|
1206
|
-
await send("Nothing running.");
|
|
1060
|
+
else await send("Nothing running.");
|
|
1207
1061
|
});
|
|
1208
1062
|
|
|
1209
1063
|
bot.onText(/\/status/, (msg) => {
|
|
1210
1064
|
if (!isAuthorized(msg)) return;
|
|
1211
1065
|
if (!currentSession) return send("No session.", { keyboard: { inline_keyboard: [[{ text: "Pick", callback_data: "show:projects" }]] } });
|
|
1212
|
-
|
|
1213
|
-
const lines = [
|
|
1066
|
+
send([
|
|
1214
1067
|
`Project: ${currentSession.name}`,
|
|
1215
|
-
`Model: ${settings.model || "default"} | Effort: ${settings.effort || "default"}
|
|
1068
|
+
`Model: ${settings.model || "default"} | Effort: ${settings.effort || "default"}`,
|
|
1216
1069
|
`Vault: ${vault.isUnlocked() ? "unlocked" : "locked"} | Crons: ${activeCrons.size}`,
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
lines.push(`Running: ${runningTasks.size} task${runningTasks.size > 1 ? "s" : ""}`);
|
|
1220
|
-
for (const [id, task] of runningTasks) {
|
|
1221
|
-
const elapsed = Math.floor((Date.now() - task.startTime) / 1000);
|
|
1222
|
-
lines.push(` ${id}: ${task.prompt}... (${elapsed}s)`);
|
|
1223
|
-
}
|
|
1224
|
-
} else if (runningProcess) {
|
|
1225
|
-
lines.push("Working...");
|
|
1226
|
-
} else {
|
|
1227
|
-
lines.push("Ready.");
|
|
1228
|
-
}
|
|
1229
|
-
send(lines.join("\n"));
|
|
1070
|
+
runningProcess ? "Working..." : "Ready.",
|
|
1071
|
+
].join("\n"));
|
|
1230
1072
|
});
|
|
1231
1073
|
|
|
1232
1074
|
bot.onText(/\/end/, (msg) => {
|
|
@@ -1362,7 +1204,16 @@ bot.on("callback_query", async (q) => {
|
|
|
1362
1204
|
if (d.startsWith("m:")) { settings.model = d.slice(2) === "default" ? null : d.slice(2); await send(`Model: ${settings.model || "default"}`); return; }
|
|
1363
1205
|
if (d.startsWith("e:")) { const e = d.slice(2); settings.effort = e === "default" ? null : e; await send(`Effort: ${settings.effort || "default"}`); return; }
|
|
1364
1206
|
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; }
|
|
1365
|
-
|
|
1207
|
+
|
|
1208
|
+
// Mode switching — writes mode file and restarts bot
|
|
1209
|
+
if (d.startsWith("mode:")) {
|
|
1210
|
+
const newMode = d.slice(5);
|
|
1211
|
+
const modeFile = path.join(CONFIG_DIR, ".bot-mode");
|
|
1212
|
+
fs.writeFileSync(modeFile, newMode);
|
|
1213
|
+
await send(`Switching to ${newMode} mode... restarting.`);
|
|
1214
|
+
setTimeout(() => process.exit(0), 500); // Let launchd/systemd restart with new mode
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1366
1217
|
|
|
1367
1218
|
// Cron presets
|
|
1368
1219
|
if (d.startsWith("cp:") && d !== "cp:clear") {
|
|
@@ -1396,7 +1247,7 @@ bot.on("voice", async (msg) => {
|
|
|
1396
1247
|
try { fs.unlinkSync(oggPath); } catch (e) {}
|
|
1397
1248
|
if (!transcript) return send("Couldn't transcribe. Try typing it.");
|
|
1398
1249
|
await send(`Heard: "${transcript}"`, { replyTo: msg.message_id });
|
|
1399
|
-
await
|
|
1250
|
+
await runClaude(transcript, currentSession.dir, msg.message_id);
|
|
1400
1251
|
} catch (err) { await send(`Voice failed: ${err.message}`); }
|
|
1401
1252
|
});
|
|
1402
1253
|
|
|
@@ -1411,7 +1262,7 @@ bot.on("audio", async (msg) => {
|
|
|
1411
1262
|
try { fs.unlinkSync(p); } catch (e) {}
|
|
1412
1263
|
if (!t) return send("Couldn't transcribe.");
|
|
1413
1264
|
await send(`Heard: "${t}"`, { replyTo: msg.message_id });
|
|
1414
|
-
await
|
|
1265
|
+
await runClaude(t, currentSession.dir, msg.message_id);
|
|
1415
1266
|
} catch (err) { await send(`Audio failed: ${err.message}`); }
|
|
1416
1267
|
});
|
|
1417
1268
|
|
|
@@ -1422,7 +1273,7 @@ bot.on("photo", async (msg) => {
|
|
|
1422
1273
|
try {
|
|
1423
1274
|
const p = await downloadFile(msg.photo[msg.photo.length - 1].file_id, ".jpg");
|
|
1424
1275
|
const caption = msg.caption || "Describe this image. If code/UI/error — explain and fix.";
|
|
1425
|
-
await
|
|
1276
|
+
await runClaude(`Image at ${p}\n\nView it, then: ${caption}`, currentSession.dir, msg.message_id);
|
|
1426
1277
|
} catch (err) { await send(`Image failed: ${err.message}`); }
|
|
1427
1278
|
});
|
|
1428
1279
|
|
|
@@ -1450,7 +1301,7 @@ bot.on("document", async (msg) => {
|
|
|
1450
1301
|
prompt = `File received: ${fileName} (${mime})\nSaved at: ${savePath}\n\nRead this file and ${caption || "summarize its contents. If it's code, explain what it does. If it's a document, give key points."}`;
|
|
1451
1302
|
}
|
|
1452
1303
|
await send(`File saved: ${fileName}`, { replyTo: msg.message_id });
|
|
1453
|
-
await
|
|
1304
|
+
await runClaude(prompt, currentSession.dir, msg.message_id);
|
|
1454
1305
|
} catch (err) { await send(`Failed: ${err.message}`); }
|
|
1455
1306
|
});
|
|
1456
1307
|
|
|
@@ -1555,7 +1406,7 @@ bot.on("message", async (msg) => {
|
|
|
1555
1406
|
}
|
|
1556
1407
|
}
|
|
1557
1408
|
|
|
1558
|
-
await
|
|
1409
|
+
await runClaude(prompt, currentSession.dir, msg.message_id);
|
|
1559
1410
|
});
|
|
1560
1411
|
|
|
1561
1412
|
// ── Startup ─────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
CONFIG_DIR="$HOME/.open-claudia"
|
|
5
|
+
ENV_FILE="$CONFIG_DIR/.env"
|
|
6
|
+
|
|
7
|
+
mkdir -p "$CONFIG_DIR"
|
|
8
|
+
|
|
9
|
+
# If .env doesn't exist and env vars are set, auto-configure
|
|
10
|
+
if [ ! -f "$ENV_FILE" ] && [ -n "$TELEGRAM_BOT_TOKEN" ]; then
|
|
11
|
+
echo "Auto-configuring from environment variables..."
|
|
12
|
+
|
|
13
|
+
cat > "$ENV_FILE" <<ENVEOF
|
|
14
|
+
TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
|
15
|
+
TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-}
|
|
16
|
+
WORKSPACE=${WORKSPACE:-$HOME/Workspace}
|
|
17
|
+
CLAUDE_PATH=${CLAUDE_PATH:-claude}
|
|
18
|
+
SOUL_FILE=$CONFIG_DIR/soul.md
|
|
19
|
+
CRONS_FILE=$CONFIG_DIR/crons.json
|
|
20
|
+
VAULT_FILE=$CONFIG_DIR/vault.enc
|
|
21
|
+
AUTH_FILE=$CONFIG_DIR/auth.json
|
|
22
|
+
ONBOARDED=true
|
|
23
|
+
WEB_UI=${WEB_UI:-true}
|
|
24
|
+
WEB_PORT=${WEB_PORT:-8080}
|
|
25
|
+
ENVEOF
|
|
26
|
+
|
|
27
|
+
# Create workspace directory
|
|
28
|
+
mkdir -p "${WORKSPACE:-$HOME/Workspace}"
|
|
29
|
+
|
|
30
|
+
# Create default soul
|
|
31
|
+
if [ ! -f "$CONFIG_DIR/soul.md" ]; then
|
|
32
|
+
cat > "$CONFIG_DIR/soul.md" <<'SOUL'
|
|
33
|
+
You are a helpful AI coding assistant running via Telegram.
|
|
34
|
+
Keep responses concise — this is a mobile screen.
|
|
35
|
+
Act on screenshots, fix bugs, implement designs.
|
|
36
|
+
SOUL
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Create empty files if missing
|
|
40
|
+
[ -f "$CONFIG_DIR/crons.json" ] || echo "[]" > "$CONFIG_DIR/crons.json"
|
|
41
|
+
[ -f "$CONFIG_DIR/auth.json" ] || echo '{"authorized":[],"pending":[]}' > "$CONFIG_DIR/auth.json"
|
|
42
|
+
|
|
43
|
+
echo "Configuration written to $ENV_FILE"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Execute the main command
|
|
47
|
+
exec "$@"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Your always-on AI coding assistant — Claude Code via Telegram",
|
|
5
5
|
"main": "bot.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"bot.js",
|
|
16
|
+
"bot-agent.js",
|
|
16
17
|
"vault.js",
|
|
17
18
|
"setup.js",
|
|
18
19
|
"web.js",
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
"bin/",
|
|
21
22
|
"k8s/",
|
|
22
23
|
"Dockerfile",
|
|
24
|
+
"docker-entrypoint.sh",
|
|
23
25
|
".dockerignore",
|
|
24
26
|
".env.example",
|
|
25
27
|
"README.md"
|
package/k8s/deployment.yaml
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
apiVersion: apps/v1
|
|
2
|
-
kind: Deployment
|
|
3
|
-
metadata:
|
|
4
|
-
name: open-claudia
|
|
5
|
-
labels:
|
|
6
|
-
app: open-claudia
|
|
7
|
-
spec:
|
|
8
|
-
replicas: 1
|
|
9
|
-
strategy:
|
|
10
|
-
type: Recreate # Only one instance can poll Telegram
|
|
11
|
-
selector:
|
|
12
|
-
matchLabels:
|
|
13
|
-
app: open-claudia
|
|
14
|
-
template:
|
|
15
|
-
metadata:
|
|
16
|
-
labels:
|
|
17
|
-
app: open-claudia
|
|
18
|
-
spec:
|
|
19
|
-
containers:
|
|
20
|
-
- name: open-claudia
|
|
21
|
-
image: registry.example.com/open-claudia:latest
|
|
22
|
-
ports:
|
|
23
|
-
- containerPort: 8080
|
|
24
|
-
name: web
|
|
25
|
-
env:
|
|
26
|
-
- name: WEB_UI
|
|
27
|
-
value: "true"
|
|
28
|
-
- name: WEB_PORT
|
|
29
|
-
value: "8080"
|
|
30
|
-
- name: ANTHROPIC_API_KEY
|
|
31
|
-
valueFrom:
|
|
32
|
-
secretKeyRef:
|
|
33
|
-
name: open-claudia-secrets
|
|
34
|
-
key: anthropic-api-key
|
|
35
|
-
optional: true
|
|
36
|
-
volumeMounts:
|
|
37
|
-
- name: data
|
|
38
|
-
mountPath: /data
|
|
39
|
-
resources:
|
|
40
|
-
requests:
|
|
41
|
-
memory: "256Mi"
|
|
42
|
-
cpu: "100m"
|
|
43
|
-
limits:
|
|
44
|
-
memory: "1Gi"
|
|
45
|
-
cpu: "1000m"
|
|
46
|
-
livenessProbe:
|
|
47
|
-
httpGet:
|
|
48
|
-
path: /api/status
|
|
49
|
-
port: 8080
|
|
50
|
-
initialDelaySeconds: 10
|
|
51
|
-
periodSeconds: 30
|
|
52
|
-
readinessProbe:
|
|
53
|
-
httpGet:
|
|
54
|
-
path: /api/status
|
|
55
|
-
port: 8080
|
|
56
|
-
initialDelaySeconds: 5
|
|
57
|
-
periodSeconds: 10
|
|
58
|
-
volumes:
|
|
59
|
-
- name: data
|
|
60
|
-
persistentVolumeClaim:
|
|
61
|
-
claimName: open-claudia-data
|
package/k8s/ingress.yaml
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
apiVersion: networking.k8s.io/v1
|
|
2
|
-
kind: Ingress
|
|
3
|
-
metadata:
|
|
4
|
-
name: open-claudia
|
|
5
|
-
labels:
|
|
6
|
-
app: open-claudia
|
|
7
|
-
annotations:
|
|
8
|
-
# Uncomment for cert-manager TLS
|
|
9
|
-
# cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
10
|
-
spec:
|
|
11
|
-
rules:
|
|
12
|
-
- host: claudia.example.com # Replace with your domain
|
|
13
|
-
http:
|
|
14
|
-
paths:
|
|
15
|
-
- path: /
|
|
16
|
-
pathType: Prefix
|
|
17
|
-
backend:
|
|
18
|
-
service:
|
|
19
|
-
name: open-claudia
|
|
20
|
-
port:
|
|
21
|
-
number: 8080
|
|
22
|
-
# Uncomment for TLS
|
|
23
|
-
# tls:
|
|
24
|
-
# - hosts:
|
|
25
|
-
# - claudia.example.com
|
|
26
|
-
# secretName: open-claudia-tls
|
package/k8s/pvc.yaml
DELETED
package/k8s/secret.yaml
DELETED