@inetafrica/open-claudia 1.7.1 → 1.7.3
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 +14 -5
- package/bin/cli.js +53 -6
- package/bot-agent.js +2 -1
- package/bot.js +114 -6
- package/health.js +391 -0
- package/package.json +2 -1
package/Dockerfile
CHANGED
|
@@ -11,6 +11,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
11
11
|
RUN curl -fsSL https://claude.ai/install.sh | sh || \
|
|
12
12
|
npm install -g @anthropic-ai/claude-code
|
|
13
13
|
|
|
14
|
+
# Create non-root user (Claude Code refuses --dangerously-skip-permissions as root)
|
|
15
|
+
RUN groupadd -g 1000 claudia && useradd -u 1000 -g 1000 -m -d /data claudia
|
|
16
|
+
|
|
14
17
|
# Create app directory
|
|
15
18
|
WORKDIR /app
|
|
16
19
|
|
|
@@ -21,6 +24,14 @@ RUN npm ci --production
|
|
|
21
24
|
# Copy app source
|
|
22
25
|
COPY . .
|
|
23
26
|
|
|
27
|
+
# Entrypoint auto-configures from env vars on first run
|
|
28
|
+
COPY docker-entrypoint.sh /usr/local/bin/
|
|
29
|
+
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
|
30
|
+
|
|
31
|
+
# Set up data directory ownership
|
|
32
|
+
RUN mkdir -p /data/Workspace /data/.open-claudia /data/.claude && \
|
|
33
|
+
chown -R claudia:claudia /data
|
|
34
|
+
|
|
24
35
|
# Config and data volume
|
|
25
36
|
ENV HOME=/data
|
|
26
37
|
ENV WEB_UI=true
|
|
@@ -29,10 +40,8 @@ VOLUME /data
|
|
|
29
40
|
|
|
30
41
|
EXPOSE 8080
|
|
31
42
|
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
|
35
|
-
ENTRYPOINT ["docker-entrypoint.sh"]
|
|
43
|
+
# Switch to non-root user
|
|
44
|
+
USER 1000
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
ENTRYPOINT ["docker-entrypoint.sh"]
|
|
38
47
|
CMD ["node", "bin/cli.js", "web"]
|
package/bin/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ const command = args[0] || "help";
|
|
|
9
9
|
|
|
10
10
|
const botDir = path.join(__dirname, "..");
|
|
11
11
|
const configDir = require(path.join(botDir, "config-dir"));
|
|
12
|
+
const health = require(path.join(botDir, "health"));
|
|
12
13
|
|
|
13
14
|
function getBotFile() {
|
|
14
15
|
const modeFile = path.join(configDir, ".bot-mode");
|
|
@@ -19,6 +20,15 @@ function getBotFile() {
|
|
|
19
20
|
return "bot.js";
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
function startBot(botFile, args) {
|
|
24
|
+
// Start web UI alongside bot if WEB_UI=true or --web flag
|
|
25
|
+
if (process.env.WEB_UI === "true" || args.includes("--web")) {
|
|
26
|
+
const { startWebServer } = require(path.join(botDir, "web.js"));
|
|
27
|
+
startWebServer();
|
|
28
|
+
}
|
|
29
|
+
require(path.join(botDir, botFile));
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
function findBotProcesses() {
|
|
23
33
|
try {
|
|
24
34
|
if (process.platform === "win32") {
|
|
@@ -49,14 +59,42 @@ switch (command) {
|
|
|
49
59
|
break;
|
|
50
60
|
|
|
51
61
|
case "start": {
|
|
62
|
+
const skipHealthCheck = args.includes("--skip-health") || args.includes("--force");
|
|
52
63
|
const botFile = getBotFile();
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
|
|
65
|
+
// Run health checks before starting (unless skipped)
|
|
66
|
+
if (!skipHealthCheck) {
|
|
67
|
+
console.log("Running health checks...");
|
|
68
|
+
health.runHealthChecks({ quick: args.includes("--quick") }).then((results) => {
|
|
69
|
+
if (!results.ok) {
|
|
70
|
+
console.error("\n" + health.formatResults(results, true));
|
|
71
|
+
console.error("\nFix the issues above or use --skip-health to bypass (not recommended).");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
if (results.warnings.length > 0) {
|
|
75
|
+
console.log("Warnings:");
|
|
76
|
+
for (const w of results.warnings) console.log(` - ${w}`);
|
|
77
|
+
}
|
|
78
|
+
console.log(`Starting Open Claudia (${botFile === "bot-agent.js" ? "agent" : "direct"} mode)...`);
|
|
79
|
+
startBot(botFile, args);
|
|
80
|
+
}).catch((err) => {
|
|
81
|
+
console.error("Health check failed:", err.message);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
console.log(`Starting Open Claudia (${botFile === "bot-agent.js" ? "agent" : "direct"} mode)...`);
|
|
86
|
+
startBot(botFile, args);
|
|
58
87
|
}
|
|
59
|
-
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case "health": {
|
|
92
|
+
console.log("Running health checks...\n");
|
|
93
|
+
const verbose = args.includes("-v") || args.includes("--verbose");
|
|
94
|
+
health.runHealthChecks({ quick: false }).then((results) => {
|
|
95
|
+
console.log(health.formatResults(results, verbose));
|
|
96
|
+
process.exit(results.ok ? 0 : 1);
|
|
97
|
+
});
|
|
60
98
|
break;
|
|
61
99
|
}
|
|
62
100
|
|
|
@@ -123,6 +161,15 @@ Commands:
|
|
|
123
161
|
open-claudia web Start with web UI for setup/config
|
|
124
162
|
open-claudia stop Stop the bot
|
|
125
163
|
open-claudia status Check if running
|
|
164
|
+
open-claudia health Run environment health checks
|
|
126
165
|
open-claudia logs View recent logs
|
|
166
|
+
|
|
167
|
+
Start options:
|
|
168
|
+
--web Also start the web UI
|
|
169
|
+
--quick Skip slow health checks (Claude auth, Telegram API)
|
|
170
|
+
--skip-health Bypass all health checks (not recommended)
|
|
171
|
+
|
|
172
|
+
Health options:
|
|
173
|
+
-v, --verbose Show detailed check results
|
|
127
174
|
`);
|
|
128
175
|
}
|
package/bot-agent.js
CHANGED
|
@@ -1018,10 +1018,11 @@ bot.onText(/\/upgrade$/, async (msg) => {
|
|
|
1018
1018
|
try {
|
|
1019
1019
|
execSync("npm install -g @inetafrica/open-claudia@latest 2>&1", {
|
|
1020
1020
|
encoding: "utf-8", timeout: 120000,
|
|
1021
|
+
cwd: process.env.HOME || require("os").homedir(),
|
|
1021
1022
|
env: { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME || require("os").homedir() },
|
|
1022
1023
|
});
|
|
1023
1024
|
// Read version from newly installed package
|
|
1024
|
-
const root = execSync("npm root -g", { encoding: "utf-8", env: { ...process.env, PATH: FULL_PATH } }).trim();
|
|
1025
|
+
const root = execSync("npm root -g", { encoding: "utf-8", cwd: process.env.HOME || require("os").homedir(), env: { ...process.env, PATH: FULL_PATH } }).trim();
|
|
1025
1026
|
const newPkg = JSON.parse(fs.readFileSync(path.join(root, "@inetafrica", "open-claudia", "package.json"), "utf-8"));
|
|
1026
1027
|
await send(`Installed v${newPkg.version}. Going offline to restart...`);
|
|
1027
1028
|
} catch (e) {
|
package/bot.js
CHANGED
|
@@ -8,8 +8,39 @@ const Vault = require("./vault");
|
|
|
8
8
|
const CONFIG_DIR = require("./config-dir");
|
|
9
9
|
|
|
10
10
|
// ── Graceful shutdown & error handling ─────────────────────────────
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
async function gracefulShutdown(signal) {
|
|
12
|
+
console.log(`Received ${signal}, shutting down gracefully...`);
|
|
13
|
+
// Stop any running Claude process
|
|
14
|
+
if (typeof runningProcess !== "undefined" && runningProcess) {
|
|
15
|
+
try {
|
|
16
|
+
process.kill(-runningProcess.pid, "SIGTERM");
|
|
17
|
+
} catch (e) {
|
|
18
|
+
try { runningProcess.kill("SIGTERM"); } catch (e2) {}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Clean up temp media files older than 1 hour
|
|
22
|
+
try {
|
|
23
|
+
const CONFIG_DIR_TEMP = require("./config-dir");
|
|
24
|
+
const mediaDir = require("path").join(CONFIG_DIR_TEMP, "media");
|
|
25
|
+
if (require("fs").existsSync(mediaDir)) {
|
|
26
|
+
const files = require("fs").readdirSync(mediaDir);
|
|
27
|
+
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
const filePath = require("path").join(mediaDir, file);
|
|
30
|
+
try {
|
|
31
|
+
const stat = require("fs").statSync(filePath);
|
|
32
|
+
if (stat.mtimeMs < oneHourAgo) {
|
|
33
|
+
require("fs").unlinkSync(filePath);
|
|
34
|
+
}
|
|
35
|
+
} catch (e) {}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (e) {}
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
43
|
+
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
13
44
|
|
|
14
45
|
// Notify user of crashes via Telegram before exiting
|
|
15
46
|
function notifyError(label, err) {
|
|
@@ -76,6 +107,34 @@ const CHAT_IDS = (config.TELEGRAM_CHAT_ID || "").split(",").map((s) => s.trim())
|
|
|
76
107
|
const CHAT_ID = CHAT_IDS[0]; // Primary owner chat for crons/notifications
|
|
77
108
|
const WORKSPACE = config.WORKSPACE;
|
|
78
109
|
const CLAUDE_PATH = config.CLAUDE_PATH;
|
|
110
|
+
|
|
111
|
+
// Validate critical config at startup
|
|
112
|
+
if (!TOKEN) { console.error("TELEGRAM_BOT_TOKEN not set"); process.exit(1); }
|
|
113
|
+
if (!CHAT_ID) { console.error("TELEGRAM_CHAT_ID not set"); process.exit(1); }
|
|
114
|
+
if (!WORKSPACE) { console.error("WORKSPACE not set"); process.exit(1); }
|
|
115
|
+
if (!CLAUDE_PATH) { console.error("CLAUDE_PATH not set"); process.exit(1); }
|
|
116
|
+
|
|
117
|
+
// Ensure workspace exists
|
|
118
|
+
if (!fs.existsSync(WORKSPACE)) {
|
|
119
|
+
try {
|
|
120
|
+
fs.mkdirSync(WORKSPACE, { recursive: true });
|
|
121
|
+
console.log(`Created workspace: ${WORKSPACE}`);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
console.error(`Failed to create workspace: ${e.message}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Verify Claude CLI exists
|
|
129
|
+
if (!fs.existsSync(CLAUDE_PATH)) {
|
|
130
|
+
const { execSync } = require("child_process");
|
|
131
|
+
try {
|
|
132
|
+
execSync(`which "${CLAUDE_PATH}" 2>/dev/null || where "${CLAUDE_PATH}" 2>nul`, { encoding: "utf-8" });
|
|
133
|
+
} catch (e) {
|
|
134
|
+
console.error(`Claude CLI not found at: ${CLAUDE_PATH}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
79
138
|
const WHISPER_CLI = config.WHISPER_CLI || "";
|
|
80
139
|
const WHISPER_MODEL = config.WHISPER_MODEL || "";
|
|
81
140
|
const FFMPEG = config.FFMPEG || "";
|
|
@@ -188,6 +247,13 @@ const FILES_DIR = path.join(CONFIG_DIR, "files");
|
|
|
188
247
|
if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
189
248
|
if (!fs.existsSync(FILES_DIR)) fs.mkdirSync(FILES_DIR, { recursive: true });
|
|
190
249
|
|
|
250
|
+
// File size limits (in bytes)
|
|
251
|
+
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB max for documents
|
|
252
|
+
const MAX_VOICE_SIZE = 10 * 1024 * 1024; // 10MB max for voice/audio
|
|
253
|
+
|
|
254
|
+
// Process timeout (30 minutes max)
|
|
255
|
+
const MAX_PROCESS_TIMEOUT = 30 * 60 * 1000;
|
|
256
|
+
|
|
191
257
|
// ── Persistent state ───────────────────────────────────────────────
|
|
192
258
|
const STATE_FILE = path.join(CONFIG_DIR, "state.json");
|
|
193
259
|
const SESSIONS_FILE = path.join(CONFIG_DIR, "sessions.json");
|
|
@@ -695,6 +761,21 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
695
761
|
const startTime = Date.now();
|
|
696
762
|
let longRunningNotified = false;
|
|
697
763
|
|
|
764
|
+
// Hard timeout to prevent runaway processes
|
|
765
|
+
const processTimeout = setTimeout(async () => {
|
|
766
|
+
if (runningProcess === proc) {
|
|
767
|
+
try {
|
|
768
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
769
|
+
setTimeout(() => {
|
|
770
|
+
try { process.kill(-proc.pid, "SIGKILL"); } catch (e) {}
|
|
771
|
+
}, 5000);
|
|
772
|
+
} catch (e) {
|
|
773
|
+
try { proc.kill("SIGKILL"); } catch (e2) {}
|
|
774
|
+
}
|
|
775
|
+
await send(`Task timed out after ${MAX_PROCESS_TIMEOUT / 60000} minutes. Stopped.`);
|
|
776
|
+
}
|
|
777
|
+
}, MAX_PROCESS_TIMEOUT);
|
|
778
|
+
|
|
698
779
|
let lastUpdate = "";
|
|
699
780
|
// Adaptive update interval: 2s for first 2min, then 5s to avoid rate limits
|
|
700
781
|
const scheduleUpdate = () => {
|
|
@@ -756,11 +837,26 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
756
837
|
}
|
|
757
838
|
});
|
|
758
839
|
|
|
759
|
-
|
|
840
|
+
let stderrBuffer = "";
|
|
841
|
+
proc.stderr.on("data", (d) => {
|
|
842
|
+
const chunk = d.toString();
|
|
843
|
+
stderrBuffer += chunk;
|
|
844
|
+
console.error("STDERR:", chunk);
|
|
845
|
+
});
|
|
760
846
|
|
|
761
847
|
proc.on("close", async (code) => {
|
|
762
848
|
runningProcess = null;
|
|
763
849
|
clearTimeout(streamInterval); streamInterval = null;
|
|
850
|
+
clearTimeout(processTimeout);
|
|
851
|
+
|
|
852
|
+
// Check for auth errors in stderr
|
|
853
|
+
const stderrLower = stderrBuffer.toLowerCase();
|
|
854
|
+
if (stderrLower.includes("unauthorized") || stderrLower.includes("auth") && stderrLower.includes("fail") ||
|
|
855
|
+
stderrLower.includes("api key") || stderrLower.includes("not logged in")) {
|
|
856
|
+
await send("Claude authentication error. Run `claude auth` to re-authenticate.");
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
764
860
|
try {
|
|
765
861
|
const finalText = assistantText || "(no output)";
|
|
766
862
|
const chunks = splitMessage(finalText);
|
|
@@ -805,8 +901,11 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
805
901
|
});
|
|
806
902
|
|
|
807
903
|
proc.on("error", async (err) => {
|
|
808
|
-
runningProcess = null;
|
|
809
|
-
|
|
904
|
+
runningProcess = null;
|
|
905
|
+
clearTimeout(streamInterval);
|
|
906
|
+
clearTimeout(processTimeout);
|
|
907
|
+
await send(`Error: ${err.message}`);
|
|
908
|
+
statusMessageId = null;
|
|
810
909
|
});
|
|
811
910
|
}
|
|
812
911
|
|
|
@@ -959,10 +1058,11 @@ bot.onText(/\/upgrade$/, async (msg) => {
|
|
|
959
1058
|
try {
|
|
960
1059
|
execSync("npm install -g @inetafrica/open-claudia@latest 2>&1", {
|
|
961
1060
|
encoding: "utf-8", timeout: 120000,
|
|
1061
|
+
cwd: process.env.HOME || require("os").homedir(),
|
|
962
1062
|
env: { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME || require("os").homedir() },
|
|
963
1063
|
});
|
|
964
1064
|
// Read version from newly installed package
|
|
965
|
-
const root = execSync("npm root -g", { encoding: "utf-8", env: { ...process.env, PATH: FULL_PATH } }).trim();
|
|
1065
|
+
const root = execSync("npm root -g", { encoding: "utf-8", cwd: process.env.HOME || require("os").homedir(), env: { ...process.env, PATH: FULL_PATH } }).trim();
|
|
966
1066
|
const newPkg = JSON.parse(fs.readFileSync(path.join(root, "@inetafrica", "open-claudia", "package.json"), "utf-8"));
|
|
967
1067
|
await send(`Installed v${newPkg.version}. Going offline to restart...`);
|
|
968
1068
|
} catch (e) {
|
|
@@ -1241,6 +1341,10 @@ bot.on("voice", async (msg) => {
|
|
|
1241
1341
|
if (!isAuthorized(msg)) return;
|
|
1242
1342
|
if (!requireSession(msg)) return;
|
|
1243
1343
|
try {
|
|
1344
|
+
// Check file size
|
|
1345
|
+
if (msg.voice.file_size && msg.voice.file_size > MAX_VOICE_SIZE) {
|
|
1346
|
+
return send(`Voice note too large (${Math.round(msg.voice.file_size / 1024 / 1024)}MB). Max: ${MAX_VOICE_SIZE / 1024 / 1024}MB`);
|
|
1347
|
+
}
|
|
1244
1348
|
bot.sendChatAction(CHAT_ID, "typing");
|
|
1245
1349
|
const oggPath = await downloadFile(msg.voice.file_id, ".ogg");
|
|
1246
1350
|
const transcript = transcribeAudio(oggPath);
|
|
@@ -1282,6 +1386,10 @@ bot.on("document", async (msg) => {
|
|
|
1282
1386
|
if (!isAuthorized(msg)) return;
|
|
1283
1387
|
if (!requireSession(msg)) return;
|
|
1284
1388
|
try {
|
|
1389
|
+
// Check file size
|
|
1390
|
+
if (msg.document.file_size && msg.document.file_size > MAX_FILE_SIZE) {
|
|
1391
|
+
return send(`File too large (${Math.round(msg.document.file_size / 1024 / 1024)}MB). Max: ${MAX_FILE_SIZE / 1024 / 1024}MB`);
|
|
1392
|
+
}
|
|
1285
1393
|
const fileName = msg.document.file_name || `file-${Date.now()}`;
|
|
1286
1394
|
const mime = msg.document.mime_type || "";
|
|
1287
1395
|
// Save with original name to files dir
|
package/health.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
const { execSync } = require("child_process");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const https = require("https");
|
|
5
|
+
const CONFIG_DIR = require("./config-dir");
|
|
6
|
+
|
|
7
|
+
const ENV_FILE = path.join(CONFIG_DIR, ".env");
|
|
8
|
+
const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
|
|
9
|
+
|
|
10
|
+
// Required env keys for the bot to function
|
|
11
|
+
const REQUIRED_ENV_KEYS = [
|
|
12
|
+
"TELEGRAM_BOT_TOKEN",
|
|
13
|
+
"TELEGRAM_CHAT_ID",
|
|
14
|
+
"WORKSPACE",
|
|
15
|
+
"CLAUDE_PATH",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// Optional but recommended
|
|
19
|
+
const OPTIONAL_ENV_KEYS = [
|
|
20
|
+
"WHISPER_CLI",
|
|
21
|
+
"WHISPER_MODEL",
|
|
22
|
+
"FFMPEG",
|
|
23
|
+
"VAULT_FILE",
|
|
24
|
+
"SOUL_FILE",
|
|
25
|
+
"CRONS_FILE",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load and parse .env file
|
|
30
|
+
*/
|
|
31
|
+
function loadEnv() {
|
|
32
|
+
if (!fs.existsSync(ENV_FILE)) return null;
|
|
33
|
+
const lines = fs.readFileSync(ENV_FILE, "utf-8").split("\n");
|
|
34
|
+
const env = {};
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const idx = line.indexOf("=");
|
|
37
|
+
if (idx > 0) env[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
38
|
+
}
|
|
39
|
+
return env;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if a binary exists at the given path
|
|
44
|
+
*/
|
|
45
|
+
function binaryExists(binPath) {
|
|
46
|
+
if (!binPath) return false;
|
|
47
|
+
try {
|
|
48
|
+
fs.accessSync(binPath, fs.constants.X_OK);
|
|
49
|
+
return true;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
// Try which/where as fallback
|
|
52
|
+
try {
|
|
53
|
+
const cmd = process.platform === "win32" ? `where "${binPath}"` : `which "${binPath}"`;
|
|
54
|
+
execSync(cmd, { encoding: "utf-8", stdio: "pipe" });
|
|
55
|
+
return true;
|
|
56
|
+
} catch (e2) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Test if Claude CLI is authenticated
|
|
64
|
+
*/
|
|
65
|
+
function testClaudeAuth(claudePath) {
|
|
66
|
+
try {
|
|
67
|
+
execSync(`"${claudePath}" -p "say ok" --max-budget-usd 0.01 --output-format text 2>&1`, {
|
|
68
|
+
encoding: "utf-8",
|
|
69
|
+
timeout: 30000,
|
|
70
|
+
stdio: "pipe",
|
|
71
|
+
});
|
|
72
|
+
return { ok: true };
|
|
73
|
+
} catch (e) {
|
|
74
|
+
const errMsg = (e.stderr || e.stdout || e.message || "").toLowerCase();
|
|
75
|
+
if (errMsg.includes("auth") || errMsg.includes("login") || errMsg.includes("api key") || errMsg.includes("unauthorized")) {
|
|
76
|
+
return { ok: false, reason: "not_authenticated", message: "Claude CLI is not authenticated. Run 'claude auth' or 'claude login'." };
|
|
77
|
+
}
|
|
78
|
+
// Other errors (like timeout) are OK - Claude is accessible
|
|
79
|
+
return { ok: true };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Test Telegram bot token validity
|
|
85
|
+
*/
|
|
86
|
+
function testTelegramToken(token) {
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
const timeout = setTimeout(() => resolve({ ok: false, reason: "timeout", message: "Telegram API request timed out" }), 15000);
|
|
89
|
+
|
|
90
|
+
https.get(`https://api.telegram.org/bot${token}/getMe`, (res) => {
|
|
91
|
+
let data = "";
|
|
92
|
+
res.on("data", (d) => { data += d; });
|
|
93
|
+
res.on("end", () => {
|
|
94
|
+
clearTimeout(timeout);
|
|
95
|
+
try {
|
|
96
|
+
const json = JSON.parse(data);
|
|
97
|
+
if (json.ok) {
|
|
98
|
+
resolve({ ok: true, botInfo: json.result });
|
|
99
|
+
} else {
|
|
100
|
+
resolve({ ok: false, reason: "invalid_token", message: `Telegram API error: ${json.description || "Invalid token"}` });
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
resolve({ ok: false, reason: "parse_error", message: "Failed to parse Telegram API response" });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}).on("error", (err) => {
|
|
107
|
+
clearTimeout(timeout);
|
|
108
|
+
resolve({ ok: false, reason: "network_error", message: `Network error: ${err.message}` });
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if directory exists and is writable
|
|
115
|
+
*/
|
|
116
|
+
function checkDirectory(dirPath, createIfMissing = false) {
|
|
117
|
+
if (!dirPath) return { ok: false, reason: "not_configured", message: "Path not configured" };
|
|
118
|
+
|
|
119
|
+
if (!fs.existsSync(dirPath)) {
|
|
120
|
+
if (createIfMissing) {
|
|
121
|
+
try {
|
|
122
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
123
|
+
return { ok: true, created: true };
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return { ok: false, reason: "create_failed", message: `Failed to create directory: ${e.message}` };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { ok: false, reason: "not_found", message: `Directory does not exist: ${dirPath}` };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check if writable
|
|
132
|
+
try {
|
|
133
|
+
const testFile = path.join(dirPath, `.write-test-${Date.now()}`);
|
|
134
|
+
fs.writeFileSync(testFile, "test");
|
|
135
|
+
fs.unlinkSync(testFile);
|
|
136
|
+
return { ok: true };
|
|
137
|
+
} catch (e) {
|
|
138
|
+
return { ok: false, reason: "not_writable", message: `Directory is not writable: ${dirPath}` };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if Node.js version is compatible
|
|
144
|
+
*/
|
|
145
|
+
function checkNodeVersion() {
|
|
146
|
+
const version = process.version;
|
|
147
|
+
const major = parseInt(version.slice(1).split(".")[0], 10);
|
|
148
|
+
if (major < 18) {
|
|
149
|
+
return { ok: false, reason: "version_too_old", message: `Node.js ${version} is too old. Requires Node.js 18 or higher.`, version };
|
|
150
|
+
}
|
|
151
|
+
return { ok: true, version };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check for conflicting bot instances
|
|
156
|
+
*/
|
|
157
|
+
function checkForConflictingProcesses() {
|
|
158
|
+
try {
|
|
159
|
+
let pids;
|
|
160
|
+
if (process.platform === "win32") {
|
|
161
|
+
const wmic = execSync('wmic process where "name=\'node.exe\'" get ProcessId,CommandLine /FORMAT:CSV 2>nul', { encoding: "utf-8" });
|
|
162
|
+
pids = [];
|
|
163
|
+
for (const line of wmic.split("\n")) {
|
|
164
|
+
if (line.includes("bot.js") && line.includes("open-claudia")) {
|
|
165
|
+
const parts = line.trim().split(",");
|
|
166
|
+
const pid = parts[parts.length - 1];
|
|
167
|
+
if (pid && pid !== String(process.pid)) pids.push(pid.trim());
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
const out = execSync('ps -eo pid,command | grep "bot\\(-agent\\)\\?\\.js" | grep "open-claudia" | grep -v grep 2>/dev/null || true', { encoding: "utf-8" });
|
|
172
|
+
pids = out.trim().split("\n")
|
|
173
|
+
.map((l) => l.trim().split(/\s+/)[0])
|
|
174
|
+
.filter((pid) => pid && pid !== String(process.pid));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (pids.length > 0) {
|
|
178
|
+
return { ok: false, reason: "conflict", message: `Another instance is already running (PID: ${pids.join(", ")})`, pids };
|
|
179
|
+
}
|
|
180
|
+
return { ok: true };
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// Can't detect - assume OK
|
|
183
|
+
return { ok: true };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check available disk space
|
|
189
|
+
*/
|
|
190
|
+
function checkDiskSpace(dirPath) {
|
|
191
|
+
try {
|
|
192
|
+
if (process.platform === "win32") {
|
|
193
|
+
// Skip on Windows for now
|
|
194
|
+
return { ok: true, skipped: true };
|
|
195
|
+
}
|
|
196
|
+
const out = execSync(`df -k "${dirPath}" | tail -1`, { encoding: "utf-8" });
|
|
197
|
+
const parts = out.trim().split(/\s+/);
|
|
198
|
+
const availableKB = parseInt(parts[3], 10);
|
|
199
|
+
const availableMB = Math.floor(availableKB / 1024);
|
|
200
|
+
|
|
201
|
+
if (availableMB < 100) {
|
|
202
|
+
return { ok: false, reason: "low_space", message: `Low disk space: ${availableMB}MB available`, availableMB };
|
|
203
|
+
}
|
|
204
|
+
return { ok: true, availableMB };
|
|
205
|
+
} catch (e) {
|
|
206
|
+
return { ok: true, skipped: true };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Validate auth.json structure
|
|
212
|
+
*/
|
|
213
|
+
function checkAuthFile() {
|
|
214
|
+
if (!fs.existsSync(AUTH_FILE)) {
|
|
215
|
+
return { ok: true, exists: false };
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
|
|
219
|
+
if (!Array.isArray(auth.authorized)) {
|
|
220
|
+
return { ok: false, reason: "invalid_structure", message: "auth.json is malformed (missing authorized array)" };
|
|
221
|
+
}
|
|
222
|
+
return { ok: true, exists: true, authorizedCount: auth.authorized.length };
|
|
223
|
+
} catch (e) {
|
|
224
|
+
return { ok: false, reason: "parse_error", message: `Failed to parse auth.json: ${e.message}` };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Run all health checks
|
|
230
|
+
* @param {Object} options - Options
|
|
231
|
+
* @param {boolean} options.quick - Skip slow checks (Claude auth, Telegram API)
|
|
232
|
+
* @param {boolean} options.fix - Attempt to fix issues (create directories)
|
|
233
|
+
* @returns {Promise<Object>} Health check results
|
|
234
|
+
*/
|
|
235
|
+
async function runHealthChecks(options = {}) {
|
|
236
|
+
const { quick = false, fix = false } = options;
|
|
237
|
+
const results = {
|
|
238
|
+
ok: true,
|
|
239
|
+
checks: {},
|
|
240
|
+
errors: [],
|
|
241
|
+
warnings: [],
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// 1. Check Node.js version
|
|
245
|
+
results.checks.node = checkNodeVersion();
|
|
246
|
+
if (!results.checks.node.ok) {
|
|
247
|
+
results.ok = false;
|
|
248
|
+
results.errors.push(results.checks.node.message);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 2. Check .env file exists and has required keys
|
|
252
|
+
const env = loadEnv();
|
|
253
|
+
if (!env) {
|
|
254
|
+
results.checks.env = { ok: false, reason: "not_found", message: "No .env file found. Run 'open-claudia setup' first." };
|
|
255
|
+
results.ok = false;
|
|
256
|
+
results.errors.push(results.checks.env.message);
|
|
257
|
+
// Can't continue without env
|
|
258
|
+
return results;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const missingKeys = REQUIRED_ENV_KEYS.filter((k) => !env[k]);
|
|
262
|
+
if (missingKeys.length > 0) {
|
|
263
|
+
results.checks.env = { ok: false, reason: "missing_keys", message: `Missing required config: ${missingKeys.join(", ")}`, missingKeys };
|
|
264
|
+
results.ok = false;
|
|
265
|
+
results.errors.push(results.checks.env.message);
|
|
266
|
+
} else {
|
|
267
|
+
results.checks.env = { ok: true };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 3. Check Claude CLI exists
|
|
271
|
+
const claudePath = env.CLAUDE_PATH;
|
|
272
|
+
if (!binaryExists(claudePath)) {
|
|
273
|
+
results.checks.claude_binary = { ok: false, reason: "not_found", message: `Claude CLI not found at: ${claudePath}` };
|
|
274
|
+
results.ok = false;
|
|
275
|
+
results.errors.push(results.checks.claude_binary.message);
|
|
276
|
+
} else {
|
|
277
|
+
results.checks.claude_binary = { ok: true, path: claudePath };
|
|
278
|
+
|
|
279
|
+
// 4. Check Claude authentication (slow)
|
|
280
|
+
if (!quick) {
|
|
281
|
+
results.checks.claude_auth = testClaudeAuth(claudePath);
|
|
282
|
+
if (!results.checks.claude_auth.ok) {
|
|
283
|
+
results.ok = false;
|
|
284
|
+
results.errors.push(results.checks.claude_auth.message);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 5. Check Telegram token (slow)
|
|
290
|
+
if (!quick && env.TELEGRAM_BOT_TOKEN) {
|
|
291
|
+
results.checks.telegram = await testTelegramToken(env.TELEGRAM_BOT_TOKEN);
|
|
292
|
+
if (!results.checks.telegram.ok) {
|
|
293
|
+
results.ok = false;
|
|
294
|
+
results.errors.push(results.checks.telegram.message);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 6. Check workspace directory
|
|
299
|
+
results.checks.workspace = checkDirectory(env.WORKSPACE, fix);
|
|
300
|
+
if (!results.checks.workspace.ok) {
|
|
301
|
+
results.ok = false;
|
|
302
|
+
results.errors.push(results.checks.workspace.message);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 7. Check config directory
|
|
306
|
+
results.checks.config_dir = checkDirectory(CONFIG_DIR, fix);
|
|
307
|
+
if (!results.checks.config_dir.ok) {
|
|
308
|
+
results.ok = false;
|
|
309
|
+
results.errors.push(results.checks.config_dir.message);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 8. Check for conflicting processes
|
|
313
|
+
results.checks.conflicts = checkForConflictingProcesses();
|
|
314
|
+
if (!results.checks.conflicts.ok) {
|
|
315
|
+
results.ok = false;
|
|
316
|
+
results.errors.push(results.checks.conflicts.message);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 9. Check disk space
|
|
320
|
+
results.checks.disk = checkDiskSpace(CONFIG_DIR);
|
|
321
|
+
if (!results.checks.disk.ok) {
|
|
322
|
+
results.warnings.push(results.checks.disk.message);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 10. Check auth.json
|
|
326
|
+
results.checks.auth_file = checkAuthFile();
|
|
327
|
+
if (!results.checks.auth_file.ok) {
|
|
328
|
+
results.warnings.push(results.checks.auth_file.message);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 11. Check optional features
|
|
332
|
+
if (env.WHISPER_CLI && !binaryExists(env.WHISPER_CLI)) {
|
|
333
|
+
results.warnings.push(`Whisper CLI not found at: ${env.WHISPER_CLI} (voice notes disabled)`);
|
|
334
|
+
}
|
|
335
|
+
if (env.FFMPEG && !binaryExists(env.FFMPEG)) {
|
|
336
|
+
results.warnings.push(`FFmpeg not found at: ${env.FFMPEG} (voice notes disabled)`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return results;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Format health check results for display
|
|
344
|
+
*/
|
|
345
|
+
function formatResults(results, verbose = false) {
|
|
346
|
+
const lines = [];
|
|
347
|
+
|
|
348
|
+
if (results.ok) {
|
|
349
|
+
lines.push("Health check passed.");
|
|
350
|
+
} else {
|
|
351
|
+
lines.push("Health check FAILED.");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (results.errors.length > 0) {
|
|
355
|
+
lines.push("\nErrors:");
|
|
356
|
+
for (const err of results.errors) {
|
|
357
|
+
lines.push(` - ${err}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (results.warnings.length > 0) {
|
|
362
|
+
lines.push("\nWarnings:");
|
|
363
|
+
for (const warn of results.warnings) {
|
|
364
|
+
lines.push(` - ${warn}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (verbose) {
|
|
369
|
+
lines.push("\nDetails:");
|
|
370
|
+
for (const [name, check] of Object.entries(results.checks)) {
|
|
371
|
+
const status = check.ok ? "OK" : "FAIL";
|
|
372
|
+
lines.push(` ${name}: ${status}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return lines.join("\n");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
module.exports = {
|
|
380
|
+
runHealthChecks,
|
|
381
|
+
formatResults,
|
|
382
|
+
loadEnv,
|
|
383
|
+
testClaudeAuth,
|
|
384
|
+
testTelegramToken,
|
|
385
|
+
checkDirectory,
|
|
386
|
+
checkNodeVersion,
|
|
387
|
+
checkForConflictingProcesses,
|
|
388
|
+
binaryExists,
|
|
389
|
+
REQUIRED_ENV_KEYS,
|
|
390
|
+
OPTIONAL_ENV_KEYS,
|
|
391
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
4
4
|
"description": "Your always-on AI coding assistant — Claude Code via Telegram",
|
|
5
5
|
"main": "bot.js",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"bot.js",
|
|
16
16
|
"bot-agent.js",
|
|
17
17
|
"vault.js",
|
|
18
|
+
"health.js",
|
|
18
19
|
"setup.js",
|
|
19
20
|
"web.js",
|
|
20
21
|
"config-dir.js",
|