@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.
Files changed (4) hide show
  1. package/bin/cli.js +7 -0
  2. package/bot.js +53 -1
  3. package/package.json +2 -1
  4. 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 CHAT_ID = config.TELEGRAM_CHAT_ID;
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.2",
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
- function testTelegramToken(token) {
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
- https.get(`https://api.telegram.org/bot${token}/getMe`, (res) => {
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(null));
91
+ }).on("error", () => resolve({ ok: false }));
59
92
  });
60
93
  }
61
94
 
62
- function sendTestMessage(token, chatId) {
95
+ function telegramPost(token, method, body) {
63
96
  return new Promise((resolve) => {
64
- const postData = `chat_id=${chatId}&text=${encodeURIComponent("Setup complete! Your Claude Code bot is connected.")}`;
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}/sendMessage`,
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).ok); } catch (e) { resolve(false); }
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 getUpdatesForChatId(token) {
84
- return new Promise((resolve) => {
85
- https.get(`https://api.telegram.org/bot${token}/getUpdates?limit=5`, (res) => {
86
- let data = "";
87
- res.on("data", (d) => { data += d; });
88
- res.on("end", () => {
89
- try {
90
- const json = JSON.parse(data);
91
- if (json.ok && json.result.length > 0) {
92
- const msg = json.result[json.result.length - 1].message;
93
- if (msg?.chat?.id) {
94
- resolve({
95
- chatId: String(msg.chat.id),
96
- firstName: msg.from?.first_name || "",
97
- lastName: msg.from?.last_name || "",
98
- username: msg.from?.username || "",
99
- });
100
- } else {
101
- resolve(null);
102
- }
103
- } else {
104
- resolve(null);
105
- }
106
- } catch (e) { resolve(null); }
107
- });
108
- }).on("error", () => resolve(null));
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
- async function main() {
229
- console.log("\n Claude Code Telegram Bot — Setup\n");
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
- // 1. Check prerequisites
232
- console.log("Checking prerequisites...\n");
299
+ const auth = loadAuth();
233
300
 
234
- const claudePath = findClaude();
235
- if (!claudePath) {
236
- console.log(" Claude Code CLI not found. Install it first:");
237
- console.log(" https://docs.anthropic.com/en/docs/claude-code");
238
- process.exit(1);
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
- // Check if Claude is authenticated
243
- try {
244
- const authCheck = execSync(`"${claudePath}" -p "say ok" --max-budget-usd 0.01 --output-format text 2>&1`, {
245
- encoding: "utf-8", timeout: 30000,
246
- });
247
- console.log(" Claude auth: OK");
248
- } catch (e) {
249
- const errMsg = (e.stderr || e.stdout || e.message || "").toLowerCase();
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
- // Other errors (like budget exceeded) mean auth is fine
256
- console.log(" Claude auth: OK");
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
- const platform = detectPlatform();
260
- console.log(` Platform: ${platform}`);
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
- // 2. Telegram credentials
270
- console.log("Telegram Setup\n");
271
- const token = await ask(" Bot token (from @BotFather): ");
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
- console.log("\n Testing token...");
274
- const botInfo = await testTelegramToken(token);
275
- if (!botInfo) {
276
- console.log(" Invalid token. Check it and try again.");
277
- process.exit(1);
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
- console.log(` Connected: @${botInfo.username} (${botInfo.first_name})\n`);
280
-
281
- console.log(" Now send any message to your bot in Telegram...");
282
- let chatId = null;
283
- let userInfo = null;
284
- for (let i = 0; i < 30; i++) {
285
- userInfo = await getUpdatesForChatId(token);
286
- if (userInfo) break;
287
- process.stdout.write(".");
288
- await new Promise((r) => setTimeout(r, 2000));
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
- if (!userInfo) {
293
- console.log(" Didn't receive a message. Enter chat ID manually:");
294
- chatId = await ask(" Chat ID: ");
295
- } else {
296
- const name = [userInfo.firstName, userInfo.lastName].filter(Boolean).join(" ");
297
- const handle = userInfo.username ? ` (@${userInfo.username})` : "";
298
- console.log(` Message received from: ${name}${handle}`);
299
- console.log(` Chat ID: ${userInfo.chatId}`);
300
- const approve = await ask(`\n Authorize this user? (y/n) [y]: `);
301
- if (approve.toLowerCase() === "n") {
302
- console.log(" Declined. Send a message from the correct account and run setup again.");
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
- chatId = userInfo.chatId;
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
- // Test sending
309
- console.log(" Sending test message...");
310
- const sent = await sendTestMessage(token, chatId);
311
- if (sent) {
312
- console.log(" Test message sent! Check your Telegram.\n");
313
- } else {
314
- console.log(" Failed to send test message. Check chat ID.\n");
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
- // 3. Workspace
318
- const defaultWorkspace = path.join(process.env.HOME, "Workspace");
319
- const workspace = (await ask(` Workspace path [${defaultWorkspace}]: `)).trim() || defaultWorkspace;
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
- // 4. Vault password
322
- console.log("\n Vault Setup");
323
- console.log(" The vault encrypts API keys and credentials.\n");
324
- const vaultPassword = await askHidden(" Set vault password: ");
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
- if (vaultPassword !== vaultConfirm) {
328
- console.log(" Passwords don't match. Try again.");
329
- process.exit(1);
596
+ state.data.workspace = workspace;
597
+ state.completedSteps.push("workspace");
598
+ saveSetupState(state);
330
599
  }
331
600
 
332
- // Create vault
333
- const vault = new Vault(VAULT_FILE);
334
- vault.create(vaultPassword);
335
- console.log(" Vault created.\n");
336
-
337
- // 5. Write .env
338
- const env = [
339
- `TELEGRAM_BOT_TOKEN=${token}`,
340
- `TELEGRAM_CHAT_ID=${chatId}`,
341
- `WORKSPACE=${workspace}`,
342
- `CLAUDE_PATH=${claudePath}`,
343
- `WHISPER_CLI=${whisperPath || ""}`,
344
- `WHISPER_MODEL=${whisperModel || ""}`,
345
- `FFMPEG=${ffmpegPath || ""}`,
346
- `VAULT_FILE=${VAULT_FILE}`,
347
- `SOUL_FILE=${SOUL_FILE}`,
348
- `CRONS_FILE=${CRONS_FILE}`,
349
- `ONBOARDED=false`,
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
- if (!fs.existsSync(SOUL_FILE)) {
361
- fs.writeFileSync(SOUL_FILE, "# Soul\n\nYou are a helpful AI coding assistant running via Telegram.\n");
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
- // 7. Daemon setup
365
- console.log(" Daemon Setup\n");
366
- const setupDaemonAnswer = await ask(" Install as background service? (y/n) [y]: ");
367
- if (setupDaemonAnswer.toLowerCase() !== "n") {
368
- await setupDaemon(platform);
369
- } else {
370
- console.log(` Run manually: node ${path.join(__dirname, "bot.js")}`);
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);