@inetafrica/open-claudia 1.0.2 → 1.0.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/bin/cli.js +7 -0
- package/bot.js +53 -1
- package/package.json +2 -1
- package/setup.js +451 -154
package/bin/cli.js
CHANGED
|
@@ -18,6 +18,12 @@ switch (command) {
|
|
|
18
18
|
require(path.join(botDir, "bot.js"));
|
|
19
19
|
break;
|
|
20
20
|
|
|
21
|
+
case "auth":
|
|
22
|
+
// Pass through to setup.js auth mode
|
|
23
|
+
process.argv = [process.argv[0], process.argv[1], "--auth"];
|
|
24
|
+
require(path.join(botDir, "setup.js"));
|
|
25
|
+
break;
|
|
26
|
+
|
|
21
27
|
case "stop":
|
|
22
28
|
try {
|
|
23
29
|
execSync('pkill -f "node.*bot.js"', { stdio: "inherit" });
|
|
@@ -51,6 +57,7 @@ Open Claudia — AI Coding Assistant via Telegram
|
|
|
51
57
|
|
|
52
58
|
Commands:
|
|
53
59
|
open-claudia setup Interactive setup wizard
|
|
60
|
+
open-claudia auth Manage chat authorizations
|
|
54
61
|
open-claudia start Start the bot
|
|
55
62
|
open-claudia stop Stop the bot
|
|
56
63
|
open-claudia status Check if running
|
package/bot.js
CHANGED
|
@@ -37,7 +37,8 @@ function saveEnvKey(key, value) {
|
|
|
37
37
|
|
|
38
38
|
const config = loadEnv();
|
|
39
39
|
const TOKEN = config.TELEGRAM_BOT_TOKEN;
|
|
40
|
-
const
|
|
40
|
+
const CHAT_IDS = (config.TELEGRAM_CHAT_ID || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
41
|
+
const CHAT_ID = CHAT_IDS[0]; // Primary owner chat for crons/notifications
|
|
41
42
|
const WORKSPACE = config.WORKSPACE;
|
|
42
43
|
const CLAUDE_PATH = config.CLAUDE_PATH;
|
|
43
44
|
const WHISPER_CLI = config.WHISPER_CLI || "";
|
|
@@ -46,6 +47,7 @@ const FFMPEG = config.FFMPEG || "";
|
|
|
46
47
|
const SOUL_FILE = config.SOUL_FILE || path.join(__dirname, "soul.md");
|
|
47
48
|
const CRONS_FILE = config.CRONS_FILE || path.join(__dirname, "crons.json");
|
|
48
49
|
const VAULT_FILE = config.VAULT_FILE || path.join(__dirname, "vault.enc");
|
|
50
|
+
const AUTH_FILE = config.AUTH_FILE || path.join(__dirname, "auth.json");
|
|
49
51
|
const BOT_DIR = __dirname;
|
|
50
52
|
|
|
51
53
|
// Detect PATH for subprocess
|
|
@@ -104,9 +106,59 @@ function resetSettings() {
|
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
function isAuthorized(msg) {
|
|
109
|
+
const chatId = String(msg.chat.id);
|
|
110
|
+
if (CHAT_IDS.includes(chatId)) return true;
|
|
111
|
+
// Also check auth.json for dynamically added chats
|
|
112
|
+
try {
|
|
113
|
+
const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
|
|
114
|
+
return auth.authorized.some((a) => a.chatId === chatId);
|
|
115
|
+
} catch (e) {}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isOwner(msg) {
|
|
107
120
|
return String(msg.chat.id) === CHAT_ID;
|
|
108
121
|
}
|
|
109
122
|
|
|
123
|
+
// ── Auth request handler (for unauthorized users) ──────────────────
|
|
124
|
+
bot.onText(/\/auth$/, async (msg) => {
|
|
125
|
+
if (isAuthorized(msg)) {
|
|
126
|
+
bot.sendMessage(msg.chat.id, "You're already authorized.");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const chatId = String(msg.chat.id);
|
|
130
|
+
const name = [msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ");
|
|
131
|
+
const username = msg.from?.username || "";
|
|
132
|
+
|
|
133
|
+
// Add to pending in auth.json
|
|
134
|
+
let auth;
|
|
135
|
+
try {
|
|
136
|
+
auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
|
|
137
|
+
} catch (e) {
|
|
138
|
+
auth = { authorized: [], pending: [] };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check if already pending
|
|
142
|
+
if (auth.pending.some((p) => p.chatId === chatId)) {
|
|
143
|
+
bot.sendMessage(msg.chat.id, "Your request is already pending. The bot owner will review it.");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
auth.pending.push({
|
|
148
|
+
chatId,
|
|
149
|
+
name,
|
|
150
|
+
username,
|
|
151
|
+
requestedAt: new Date().toISOString(),
|
|
152
|
+
});
|
|
153
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2));
|
|
154
|
+
|
|
155
|
+
bot.sendMessage(msg.chat.id, "Access requested! The bot owner will review your request.");
|
|
156
|
+
|
|
157
|
+
// Notify owner
|
|
158
|
+
const label = username ? `@${username}` : name;
|
|
159
|
+
bot.sendMessage(CHAT_ID, `New auth request from ${label} (${chatId}).\nRun 'open-claudia auth' to approve or deny.`);
|
|
160
|
+
});
|
|
161
|
+
|
|
110
162
|
// ── Onboarding ──────────────────────────────────────────────────────
|
|
111
163
|
let onboardingStep = null; // null | "name" | "role" | "style" | "done"
|
|
112
164
|
let onboardingData = {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Your always-on AI coding assistant — Claude Code via Telegram",
|
|
5
5
|
"main": "bot.js",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"bin/",
|
|
19
19
|
"soul.md",
|
|
20
20
|
"crons.json",
|
|
21
|
+
"auth.json",
|
|
21
22
|
".env.example",
|
|
22
23
|
"README.md"
|
|
23
24
|
],
|
package/setup.js
CHANGED
|
@@ -4,6 +4,7 @@ const readline = require("readline");
|
|
|
4
4
|
const https = require("https");
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
|
+
const crypto = require("crypto");
|
|
7
8
|
const { execSync } = require("child_process");
|
|
8
9
|
const Vault = require("./vault");
|
|
9
10
|
|
|
@@ -11,6 +12,8 @@ const ENV_FILE = path.join(__dirname, ".env");
|
|
|
11
12
|
const VAULT_FILE = path.join(__dirname, "vault.enc");
|
|
12
13
|
const SOUL_FILE = path.join(__dirname, "soul.md");
|
|
13
14
|
const CRONS_FILE = path.join(__dirname, "crons.json");
|
|
15
|
+
const AUTH_FILE = path.join(__dirname, "auth.json");
|
|
16
|
+
const SETUP_STATE_FILE = path.join(__dirname, ".setup-state.json");
|
|
14
17
|
|
|
15
18
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
16
19
|
const ask = (q) => new Promise((r) => rl.question(q, r));
|
|
@@ -44,71 +47,117 @@ const askHidden = (q) => new Promise((resolve) => {
|
|
|
44
47
|
stdin.on("data", onData);
|
|
45
48
|
});
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
// ── Auth file helpers ──────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
function loadAuth() {
|
|
53
|
+
if (fs.existsSync(AUTH_FILE)) {
|
|
54
|
+
try { return JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8")); } catch (e) {}
|
|
55
|
+
}
|
|
56
|
+
return { authorized: [], pending: [] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function saveAuth(auth) {
|
|
60
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Setup state helpers ────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
function loadSetupState() {
|
|
66
|
+
if (fs.existsSync(SETUP_STATE_FILE)) {
|
|
67
|
+
try { return JSON.parse(fs.readFileSync(SETUP_STATE_FILE, "utf-8")); } catch (e) {}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function saveSetupState(state) {
|
|
73
|
+
fs.writeFileSync(SETUP_STATE_FILE, JSON.stringify(state, null, 2));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function clearSetupState() {
|
|
77
|
+
if (fs.existsSync(SETUP_STATE_FILE)) fs.unlinkSync(SETUP_STATE_FILE);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Telegram helpers ───────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
function telegramGet(token, method, params = "") {
|
|
48
83
|
return new Promise((resolve) => {
|
|
49
|
-
|
|
84
|
+
const qs = params ? `?${params}` : "";
|
|
85
|
+
https.get(`https://api.telegram.org/bot${token}/${method}${qs}`, (res) => {
|
|
50
86
|
let data = "";
|
|
51
87
|
res.on("data", (d) => { data += d; });
|
|
52
88
|
res.on("end", () => {
|
|
53
|
-
try {
|
|
54
|
-
const json = JSON.parse(data);
|
|
55
|
-
resolve(json.ok ? json.result : null);
|
|
56
|
-
} catch (e) { resolve(null); }
|
|
89
|
+
try { resolve(JSON.parse(data)); } catch (e) { resolve({ ok: false }); }
|
|
57
90
|
});
|
|
58
|
-
}).on("error", () => resolve(
|
|
91
|
+
}).on("error", () => resolve({ ok: false }));
|
|
59
92
|
});
|
|
60
93
|
}
|
|
61
94
|
|
|
62
|
-
function
|
|
95
|
+
function telegramPost(token, method, body) {
|
|
63
96
|
return new Promise((resolve) => {
|
|
64
|
-
const postData =
|
|
97
|
+
const postData = Object.entries(body).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
65
98
|
const req = https.request({
|
|
66
99
|
hostname: "api.telegram.org",
|
|
67
|
-
path: `/bot${token}
|
|
100
|
+
path: `/bot${token}/${method}`,
|
|
68
101
|
method: "POST",
|
|
69
102
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
70
103
|
}, (res) => {
|
|
71
104
|
let data = "";
|
|
72
105
|
res.on("data", (d) => { data += d; });
|
|
73
106
|
res.on("end", () => {
|
|
74
|
-
try { resolve(JSON.parse(data)
|
|
107
|
+
try { resolve(JSON.parse(data)); } catch (e) { resolve({ ok: false }); }
|
|
75
108
|
});
|
|
76
109
|
});
|
|
77
|
-
req.on("error", () => resolve(false));
|
|
110
|
+
req.on("error", () => resolve({ ok: false }));
|
|
78
111
|
req.write(postData);
|
|
79
112
|
req.end();
|
|
80
113
|
});
|
|
81
114
|
}
|
|
82
115
|
|
|
83
|
-
function
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
function testTelegramToken(token) {
|
|
117
|
+
return telegramGet(token, "getMe").then((r) => r.ok ? r.result : null);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function sendMessage(token, chatId, text) {
|
|
121
|
+
return telegramPost(token, "sendMessage", { chat_id: chatId, text }).then((r) => r.ok);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function flushUpdates(token) {
|
|
125
|
+
const res = await telegramGet(token, "getUpdates", "offset=-1");
|
|
126
|
+
if (res.ok && res.result.length > 0) {
|
|
127
|
+
const lastId = res.result[res.result.length - 1].update_id;
|
|
128
|
+
await telegramGet(token, "getUpdates", `offset=${lastId + 1}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function waitForAuthCode(token, code, timeoutSec = 60) {
|
|
133
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
134
|
+
while (Date.now() < deadline) {
|
|
135
|
+
const res = await telegramGet(token, "getUpdates", "limit=10&timeout=2");
|
|
136
|
+
if (res.ok && res.result.length > 0) {
|
|
137
|
+
for (const update of res.result) {
|
|
138
|
+
const msg = update.message;
|
|
139
|
+
if (msg?.text?.trim() === code) {
|
|
140
|
+
// Acknowledge this update
|
|
141
|
+
await telegramGet(token, "getUpdates", `offset=${update.update_id + 1}`);
|
|
142
|
+
return {
|
|
143
|
+
chatId: String(msg.chat.id),
|
|
144
|
+
firstName: msg.from?.first_name || "",
|
|
145
|
+
lastName: msg.from?.last_name || "",
|
|
146
|
+
username: msg.from?.username || "",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Acknowledge all updates
|
|
151
|
+
const lastId = res.result[res.result.length - 1].update_id;
|
|
152
|
+
await telegramGet(token, "getUpdates", `offset=${lastId + 1}`);
|
|
153
|
+
}
|
|
154
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
110
157
|
}
|
|
111
158
|
|
|
159
|
+
// ── System detection ───────────────────────────────────────────────
|
|
160
|
+
|
|
112
161
|
function detectPlatform() {
|
|
113
162
|
const platform = process.platform;
|
|
114
163
|
if (platform === "darwin") return "macos";
|
|
@@ -144,6 +193,8 @@ function findWhisperModel() {
|
|
|
144
193
|
return null;
|
|
145
194
|
}
|
|
146
195
|
|
|
196
|
+
// ── Daemon setup ───────────────────────────────────────────────────
|
|
197
|
+
|
|
147
198
|
async function setupDaemon(platform) {
|
|
148
199
|
const nodePath = process.execPath;
|
|
149
200
|
const botPath = path.join(__dirname, "bot.js");
|
|
@@ -225,155 +276,401 @@ WantedBy=multi-user.target`;
|
|
|
225
276
|
return false;
|
|
226
277
|
}
|
|
227
278
|
|
|
228
|
-
|
|
229
|
-
|
|
279
|
+
// ── Auth subcommand ────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
async function runAuth(token) {
|
|
282
|
+
if (!token) {
|
|
283
|
+
// Try to load from .env
|
|
284
|
+
if (fs.existsSync(ENV_FILE)) {
|
|
285
|
+
const lines = fs.readFileSync(ENV_FILE, "utf-8").split("\n");
|
|
286
|
+
for (const line of lines) {
|
|
287
|
+
const idx = line.indexOf("=");
|
|
288
|
+
if (idx > 0 && line.slice(0, idx).trim() === "TELEGRAM_BOT_TOKEN") {
|
|
289
|
+
token = line.slice(idx + 1).trim();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (!token) {
|
|
294
|
+
console.log(" No .env found. Run 'open-claudia setup' first.");
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
230
298
|
|
|
231
|
-
|
|
232
|
-
console.log("Checking prerequisites...\n");
|
|
299
|
+
const auth = loadAuth();
|
|
233
300
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
301
|
+
console.log("\n Open Claudia — Chat Authorization\n");
|
|
302
|
+
|
|
303
|
+
// Show current authorized chats
|
|
304
|
+
if (auth.authorized.length > 0) {
|
|
305
|
+
console.log(" Authorized chats:");
|
|
306
|
+
for (const a of auth.authorized) {
|
|
307
|
+
const label = a.username ? `@${a.username}` : a.name || a.chatId;
|
|
308
|
+
const owner = a.isOwner ? " (owner)" : "";
|
|
309
|
+
console.log(` ${a.chatId} — ${label}${owner}`);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
console.log(" No authorized chats.");
|
|
239
313
|
}
|
|
240
|
-
console.log(` Claude CLI: ${claudePath}`);
|
|
241
314
|
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (errMsg.includes("auth") || errMsg.includes("login") || errMsg.includes("api key") || errMsg.includes("unauthorized")) {
|
|
251
|
-
console.log(" Claude auth: NOT LOGGED IN");
|
|
252
|
-
console.log(" Run 'claude auth' or 'claude login' to authenticate first.");
|
|
253
|
-
process.exit(1);
|
|
315
|
+
// Show pending requests
|
|
316
|
+
if (auth.pending.length > 0) {
|
|
317
|
+
console.log(`\n Pending requests (${auth.pending.length}):\n`);
|
|
318
|
+
for (let i = 0; i < auth.pending.length; i++) {
|
|
319
|
+
const p = auth.pending[i];
|
|
320
|
+
const label = p.username ? `@${p.username}` : p.name || p.chatId;
|
|
321
|
+
const time = new Date(p.requestedAt).toLocaleString();
|
|
322
|
+
console.log(` ${i + 1}. ${label} (${p.chatId}) — requested ${time}`);
|
|
254
323
|
}
|
|
255
|
-
|
|
256
|
-
console.log("
|
|
324
|
+
|
|
325
|
+
console.log("");
|
|
326
|
+
const action = await ask(" Approve/deny? (e.g. 'approve 1', 'deny 2', or 'skip'): ");
|
|
327
|
+
const parts = action.trim().toLowerCase().split(/\s+/);
|
|
328
|
+
|
|
329
|
+
if (parts[0] === "approve" && parts[1]) {
|
|
330
|
+
const idx = parseInt(parts[1], 10) - 1;
|
|
331
|
+
if (idx >= 0 && idx < auth.pending.length) {
|
|
332
|
+
const approved = auth.pending.splice(idx, 1)[0];
|
|
333
|
+
auth.authorized.push({
|
|
334
|
+
chatId: approved.chatId,
|
|
335
|
+
name: approved.name,
|
|
336
|
+
username: approved.username,
|
|
337
|
+
isOwner: false,
|
|
338
|
+
authorizedAt: new Date().toISOString(),
|
|
339
|
+
});
|
|
340
|
+
saveAuth(auth);
|
|
341
|
+
|
|
342
|
+
// Update TELEGRAM_CHAT_ID in .env
|
|
343
|
+
const allIds = auth.authorized.map((a) => a.chatId).join(",");
|
|
344
|
+
updateEnvKey("TELEGRAM_CHAT_ID", allIds);
|
|
345
|
+
|
|
346
|
+
// Notify the approved user
|
|
347
|
+
const label = approved.username ? `@${approved.username}` : approved.name;
|
|
348
|
+
await sendMessage(token, approved.chatId, "Your access has been approved! You can now use the bot. Send /start to begin.");
|
|
349
|
+
console.log(`\n Approved ${label}. They've been notified.`);
|
|
350
|
+
} else {
|
|
351
|
+
console.log(" Invalid number.");
|
|
352
|
+
}
|
|
353
|
+
} else if (parts[0] === "deny" && parts[1]) {
|
|
354
|
+
const idx = parseInt(parts[1], 10) - 1;
|
|
355
|
+
if (idx >= 0 && idx < auth.pending.length) {
|
|
356
|
+
const denied = auth.pending.splice(idx, 1)[0];
|
|
357
|
+
saveAuth(auth);
|
|
358
|
+
await sendMessage(token, denied.chatId, "Your access request was denied.");
|
|
359
|
+
const label = denied.username ? `@${denied.username}` : denied.name;
|
|
360
|
+
console.log(`\n Denied ${label}.`);
|
|
361
|
+
} else {
|
|
362
|
+
console.log(" Invalid number.");
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
console.log(" Skipped.");
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
console.log("\n No pending requests.");
|
|
257
369
|
}
|
|
258
370
|
|
|
259
|
-
|
|
260
|
-
console.log(
|
|
371
|
+
// Option to add a new chat
|
|
372
|
+
console.log("");
|
|
373
|
+
const addNew = await ask(" Authorize a new chat? (y/n) [n]: ");
|
|
374
|
+
if (addNew.toLowerCase() === "y") {
|
|
375
|
+
await authNewChat(token, auth);
|
|
376
|
+
}
|
|
261
377
|
|
|
262
|
-
const ffmpegPath = findFfmpeg();
|
|
263
|
-
const whisperPath = findWhisper();
|
|
264
|
-
const whisperModel = findWhisperModel();
|
|
265
|
-
console.log(` FFmpeg: ${ffmpegPath || "not found (voice notes disabled)"}`);
|
|
266
|
-
console.log(` Whisper: ${whisperPath || "not found (voice notes disabled)"}`);
|
|
267
378
|
console.log("");
|
|
379
|
+
rl.close();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function authNewChat(token, auth) {
|
|
383
|
+
const code = crypto.randomBytes(3).toString("hex").toUpperCase();
|
|
384
|
+
|
|
385
|
+
await flushUpdates(token);
|
|
268
386
|
|
|
269
|
-
|
|
270
|
-
console.log(
|
|
271
|
-
|
|
387
|
+
console.log(`\n Send this code to the bot in Telegram:\n`);
|
|
388
|
+
console.log(` ${code}\n`);
|
|
389
|
+
console.log(" Waiting for code (60s)...");
|
|
272
390
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (!
|
|
276
|
-
console.log("
|
|
277
|
-
|
|
391
|
+
const userInfo = await waitForAuthCode(token, code, 60);
|
|
392
|
+
|
|
393
|
+
if (!userInfo) {
|
|
394
|
+
console.log(" Timed out. No matching code received.");
|
|
395
|
+
return;
|
|
278
396
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
397
|
+
|
|
398
|
+
const name = [userInfo.firstName, userInfo.lastName].filter(Boolean).join(" ");
|
|
399
|
+
const handle = userInfo.username ? ` (@${userInfo.username})` : "";
|
|
400
|
+
console.log(`\n Code received from: ${name}${handle}`);
|
|
401
|
+
console.log(` Chat ID: ${userInfo.chatId}`);
|
|
402
|
+
|
|
403
|
+
// Check if already authorized
|
|
404
|
+
if (auth.authorized.some((a) => a.chatId === userInfo.chatId)) {
|
|
405
|
+
console.log(" Already authorized.");
|
|
406
|
+
return;
|
|
289
407
|
}
|
|
290
|
-
console.log("");
|
|
291
408
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
409
|
+
const approve = await ask(" Authorize this chat? (y/n) [y]: ");
|
|
410
|
+
if (approve.toLowerCase() === "n") {
|
|
411
|
+
console.log(" Declined.");
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
auth.authorized.push({
|
|
416
|
+
chatId: userInfo.chatId,
|
|
417
|
+
name,
|
|
418
|
+
username: userInfo.username,
|
|
419
|
+
isOwner: false,
|
|
420
|
+
authorizedAt: new Date().toISOString(),
|
|
421
|
+
});
|
|
422
|
+
saveAuth(auth);
|
|
423
|
+
|
|
424
|
+
// Update .env
|
|
425
|
+
const allIds = auth.authorized.map((a) => a.chatId).join(",");
|
|
426
|
+
updateEnvKey("TELEGRAM_CHAT_ID", allIds);
|
|
427
|
+
|
|
428
|
+
await sendMessage(token, userInfo.chatId, "You've been authorized! Send /start to begin.");
|
|
429
|
+
console.log(" Authorized and notified.");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function updateEnvKey(key, value) {
|
|
433
|
+
if (!fs.existsSync(ENV_FILE)) return;
|
|
434
|
+
const content = fs.readFileSync(ENV_FILE, "utf-8");
|
|
435
|
+
const lines = content.split("\n");
|
|
436
|
+
let found = false;
|
|
437
|
+
const updated = lines.map((line) => {
|
|
438
|
+
if (line.startsWith(key + "=")) { found = true; return `${key}=${value}`; }
|
|
439
|
+
return line;
|
|
440
|
+
});
|
|
441
|
+
if (!found) updated.push(`${key}=${value}`);
|
|
442
|
+
fs.writeFileSync(ENV_FILE, updated.join("\n"));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── Main setup (resumable) ─────────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
const STEPS = ["prerequisites", "telegram", "auth", "workspace", "vault", "config", "daemon"];
|
|
448
|
+
|
|
449
|
+
async function main() {
|
|
450
|
+
// Check if this is an auth subcommand
|
|
451
|
+
const args = process.argv.slice(2);
|
|
452
|
+
if (args.includes("--auth") || args.includes("auth")) {
|
|
453
|
+
await runAuth();
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
console.log("\n Claude Code Telegram Bot — Setup\n");
|
|
458
|
+
|
|
459
|
+
// Load or create state
|
|
460
|
+
let state = loadSetupState() || { completedSteps: [], data: {} };
|
|
461
|
+
const resuming = state.completedSteps.length > 0;
|
|
462
|
+
|
|
463
|
+
if (resuming) {
|
|
464
|
+
const nextStep = STEPS.find((s) => !state.completedSteps.includes(s));
|
|
465
|
+
console.log(` Resuming setup from: ${nextStep}\n`);
|
|
466
|
+
const cont = await ask(" Continue previous setup? (y/n) [y]: ");
|
|
467
|
+
if (cont.toLowerCase() === "n") {
|
|
468
|
+
state = { completedSteps: [], data: {} };
|
|
469
|
+
clearSetupState();
|
|
470
|
+
console.log(" Starting fresh.\n");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ── Step: Prerequisites ──────────────────────────────────────────
|
|
475
|
+
if (!state.completedSteps.includes("prerequisites")) {
|
|
476
|
+
console.log("Checking prerequisites...\n");
|
|
477
|
+
|
|
478
|
+
const claudePath = findClaude();
|
|
479
|
+
if (!claudePath) {
|
|
480
|
+
console.log(" Claude Code CLI not found. Install it first:");
|
|
481
|
+
console.log(" https://docs.anthropic.com/en/docs/claude-code");
|
|
303
482
|
process.exit(1);
|
|
304
483
|
}
|
|
305
|
-
|
|
484
|
+
console.log(` Claude CLI: ${claudePath}`);
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
execSync(`"${claudePath}" -p "say ok" --max-budget-usd 0.01 --output-format text 2>&1`, {
|
|
488
|
+
encoding: "utf-8", timeout: 30000,
|
|
489
|
+
});
|
|
490
|
+
console.log(" Claude auth: OK");
|
|
491
|
+
} catch (e) {
|
|
492
|
+
const errMsg = (e.stderr || e.stdout || e.message || "").toLowerCase();
|
|
493
|
+
if (errMsg.includes("auth") || errMsg.includes("login") || errMsg.includes("api key") || errMsg.includes("unauthorized")) {
|
|
494
|
+
console.log(" Claude auth: NOT LOGGED IN");
|
|
495
|
+
console.log(" Run 'claude auth' or 'claude login' to authenticate first.");
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
console.log(" Claude auth: OK");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const platform = detectPlatform();
|
|
502
|
+
console.log(` Platform: ${platform}`);
|
|
503
|
+
|
|
504
|
+
const ffmpegPath = findFfmpeg();
|
|
505
|
+
const whisperPath = findWhisper();
|
|
506
|
+
const whisperModel = findWhisperModel();
|
|
507
|
+
console.log(` FFmpeg: ${ffmpegPath || "not found (voice notes disabled)"}`);
|
|
508
|
+
console.log(` Whisper: ${whisperPath || "not found (voice notes disabled)"}`);
|
|
509
|
+
console.log("");
|
|
510
|
+
|
|
511
|
+
state.data.claudePath = claudePath;
|
|
512
|
+
state.data.platform = platform;
|
|
513
|
+
state.data.ffmpegPath = ffmpegPath || "";
|
|
514
|
+
state.data.whisperPath = whisperPath || "";
|
|
515
|
+
state.data.whisperModel = whisperModel || "";
|
|
516
|
+
state.completedSteps.push("prerequisites");
|
|
517
|
+
saveSetupState(state);
|
|
306
518
|
}
|
|
307
519
|
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
520
|
+
// ── Step: Telegram token ─────────────────────────────────────────
|
|
521
|
+
if (!state.completedSteps.includes("telegram")) {
|
|
522
|
+
console.log("Telegram Setup\n");
|
|
523
|
+
const token = await ask(" Bot token (from @BotFather): ");
|
|
524
|
+
|
|
525
|
+
console.log("\n Testing token...");
|
|
526
|
+
const botInfo = await testTelegramToken(token);
|
|
527
|
+
if (!botInfo) {
|
|
528
|
+
console.log(" Invalid token. Check it and try again.");
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
console.log(` Connected: @${botInfo.username} (${botInfo.first_name})\n`);
|
|
532
|
+
|
|
533
|
+
state.data.token = token;
|
|
534
|
+
state.data.botUsername = botInfo.username;
|
|
535
|
+
state.completedSteps.push("telegram");
|
|
536
|
+
saveSetupState(state);
|
|
315
537
|
}
|
|
316
538
|
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
539
|
+
// ── Step: Auth (code-based) ──────────────────────────────────────
|
|
540
|
+
if (!state.completedSteps.includes("auth")) {
|
|
541
|
+
const token = state.data.token;
|
|
542
|
+
const code = crypto.randomBytes(3).toString("hex").toUpperCase();
|
|
543
|
+
|
|
544
|
+
// Flush old updates
|
|
545
|
+
await flushUpdates(token);
|
|
546
|
+
|
|
547
|
+
console.log(" Chat Authorization\n");
|
|
548
|
+
console.log(` Send this code to your bot in Telegram to verify your identity:\n`);
|
|
549
|
+
console.log(` ${code}\n`);
|
|
550
|
+
console.log(" Waiting for code (60s)...");
|
|
551
|
+
|
|
552
|
+
const userInfo = await waitForAuthCode(token, code, 60);
|
|
553
|
+
|
|
554
|
+
if (!userInfo) {
|
|
555
|
+
console.log("\n Timed out. No matching code received.");
|
|
556
|
+
console.log(" Run 'open-claudia setup' to retry from this step.");
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const name = [userInfo.firstName, userInfo.lastName].filter(Boolean).join(" ");
|
|
561
|
+
const handle = userInfo.username ? ` (@${userInfo.username})` : "";
|
|
562
|
+
console.log(`\n Verified: ${name}${handle}`);
|
|
563
|
+
console.log(` Chat ID: ${userInfo.chatId}\n`);
|
|
564
|
+
|
|
565
|
+
// Send confirmation
|
|
566
|
+
console.log(" Sending test message...");
|
|
567
|
+
const sent = await sendMessage(token, userInfo.chatId, "Setup verified! Your Claude Code bot is connected.");
|
|
568
|
+
if (sent) {
|
|
569
|
+
console.log(" Test message sent! Check your Telegram.\n");
|
|
570
|
+
} else {
|
|
571
|
+
console.log(" Failed to send test message. Check chat ID.\n");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Save to auth.json as owner
|
|
575
|
+
const auth = loadAuth();
|
|
576
|
+
auth.authorized = auth.authorized.filter((a) => a.chatId !== userInfo.chatId);
|
|
577
|
+
auth.authorized.push({
|
|
578
|
+
chatId: userInfo.chatId,
|
|
579
|
+
name,
|
|
580
|
+
username: userInfo.username,
|
|
581
|
+
isOwner: true,
|
|
582
|
+
authorizedAt: new Date().toISOString(),
|
|
583
|
+
});
|
|
584
|
+
saveAuth(auth);
|
|
585
|
+
|
|
586
|
+
state.data.chatId = userInfo.chatId;
|
|
587
|
+
state.completedSteps.push("auth");
|
|
588
|
+
saveSetupState(state);
|
|
589
|
+
}
|
|
320
590
|
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const vaultConfirm = await askHidden(" Confirm password: ");
|
|
591
|
+
// ── Step: Workspace ──────────────────────────────────────────────
|
|
592
|
+
if (!state.completedSteps.includes("workspace")) {
|
|
593
|
+
const defaultWorkspace = path.join(process.env.HOME, "Workspace");
|
|
594
|
+
const workspace = (await ask(` Workspace path [${defaultWorkspace}]: `)).trim() || defaultWorkspace;
|
|
326
595
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
596
|
+
state.data.workspace = workspace;
|
|
597
|
+
state.completedSteps.push("workspace");
|
|
598
|
+
saveSetupState(state);
|
|
330
599
|
}
|
|
331
600
|
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
].join("\n");
|
|
351
|
-
|
|
352
|
-
fs.writeFileSync(ENV_FILE, env);
|
|
353
|
-
console.log(` Config saved: ${ENV_FILE}\n`);
|
|
354
|
-
|
|
355
|
-
// 6. Create default files if missing
|
|
356
|
-
if (!fs.existsSync(CRONS_FILE)) {
|
|
357
|
-
fs.writeFileSync(CRONS_FILE, "[]");
|
|
601
|
+
// ── Step: Vault ──────────────────────────────────────────────────
|
|
602
|
+
if (!state.completedSteps.includes("vault")) {
|
|
603
|
+
console.log("\n Vault Setup");
|
|
604
|
+
console.log(" The vault encrypts API keys and credentials.\n");
|
|
605
|
+
const vaultPassword = await askHidden(" Set vault password: ");
|
|
606
|
+
const vaultConfirm = await askHidden(" Confirm password: ");
|
|
607
|
+
|
|
608
|
+
if (vaultPassword !== vaultConfirm) {
|
|
609
|
+
console.log(" Passwords don't match. Run setup again to retry this step.");
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const vault = new Vault(VAULT_FILE);
|
|
614
|
+
vault.create(vaultPassword);
|
|
615
|
+
console.log(" Vault created.\n");
|
|
616
|
+
|
|
617
|
+
state.completedSteps.push("vault");
|
|
618
|
+
saveSetupState(state);
|
|
358
619
|
}
|
|
359
620
|
|
|
360
|
-
|
|
361
|
-
|
|
621
|
+
// ── Step: Write config ───────────────────────────────────────────
|
|
622
|
+
if (!state.completedSteps.includes("config")) {
|
|
623
|
+
const d = state.data;
|
|
624
|
+
const env = [
|
|
625
|
+
`TELEGRAM_BOT_TOKEN=${d.token}`,
|
|
626
|
+
`TELEGRAM_CHAT_ID=${d.chatId}`,
|
|
627
|
+
`WORKSPACE=${d.workspace}`,
|
|
628
|
+
`CLAUDE_PATH=${d.claudePath}`,
|
|
629
|
+
`WHISPER_CLI=${d.whisperPath}`,
|
|
630
|
+
`WHISPER_MODEL=${d.whisperModel}`,
|
|
631
|
+
`FFMPEG=${d.ffmpegPath}`,
|
|
632
|
+
`VAULT_FILE=${VAULT_FILE}`,
|
|
633
|
+
`SOUL_FILE=${SOUL_FILE}`,
|
|
634
|
+
`CRONS_FILE=${CRONS_FILE}`,
|
|
635
|
+
`AUTH_FILE=${AUTH_FILE}`,
|
|
636
|
+
`ONBOARDED=false`,
|
|
637
|
+
].join("\n");
|
|
638
|
+
|
|
639
|
+
fs.writeFileSync(ENV_FILE, env);
|
|
640
|
+
console.log(` Config saved: ${ENV_FILE}\n`);
|
|
641
|
+
|
|
642
|
+
if (!fs.existsSync(CRONS_FILE)) fs.writeFileSync(CRONS_FILE, "[]");
|
|
643
|
+
if (!fs.existsSync(SOUL_FILE)) {
|
|
644
|
+
fs.writeFileSync(SOUL_FILE, "# Soul\n\nYou are a helpful AI coding assistant running via Telegram.\n");
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
state.completedSteps.push("config");
|
|
648
|
+
saveSetupState(state);
|
|
362
649
|
}
|
|
363
650
|
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
651
|
+
// ── Step: Daemon ─────────────────────────────────────────────────
|
|
652
|
+
if (!state.completedSteps.includes("daemon")) {
|
|
653
|
+
console.log(" Daemon Setup\n");
|
|
654
|
+
const setupDaemonAnswer = await ask(" Install as background service? (y/n) [y]: ");
|
|
655
|
+
if (setupDaemonAnswer.toLowerCase() !== "n") {
|
|
656
|
+
await setupDaemon(state.data.platform);
|
|
657
|
+
} else {
|
|
658
|
+
console.log(` Run manually: node ${path.join(__dirname, "bot.js")}`);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
state.completedSteps.push("daemon");
|
|
662
|
+
saveSetupState(state);
|
|
371
663
|
}
|
|
372
664
|
|
|
665
|
+
// Done — clean up state file
|
|
666
|
+
clearSetupState();
|
|
373
667
|
console.log("\n Setup complete! Start chatting with your bot in Telegram.\n");
|
|
374
668
|
rl.close();
|
|
375
669
|
}
|
|
376
670
|
|
|
671
|
+
// Allow running auth directly: node setup.js auth
|
|
672
|
+
module.exports = { runAuth, loadAuth, saveAuth, AUTH_FILE };
|
|
673
|
+
|
|
377
674
|
main().catch((e) => {
|
|
378
675
|
console.error(e);
|
|
379
676
|
process.exit(1);
|