@inetafrica/open-claudia 1.0.2 → 1.0.4

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