@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 +9 -1
- package/bot.js +59 -6
- package/config-dir.js +11 -0
- package/package.json +2 -3
- package/setup.js +460 -162
- package/crons.json +0 -1
- package/soul.md +0 -39
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
47
|
-
const CRONS_FILE = config.CRONS_FILE || path.join(
|
|
48
|
-
const VAULT_FILE = config.VAULT_FILE || path.join(
|
|
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.
|
|
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(
|
|
11
|
-
const VAULT_FILE = path.join(
|
|
12
|
-
const SOUL_FILE = path.join(
|
|
13
|
-
const CRONS_FILE = path.join(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
92
|
+
}).on("error", () => resolve({ ok: false }));
|
|
59
93
|
});
|
|
60
94
|
}
|
|
61
95
|
|
|
62
|
-
function
|
|
96
|
+
function telegramPost(token, method, body) {
|
|
63
97
|
return new Promise((resolve) => {
|
|
64
|
-
const postData =
|
|
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}
|
|
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)
|
|
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
|
|
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
|
-
|
|
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(
|
|
224
|
+
<string>${path.join(CONFIG_DIR, "bot.log")}</string>
|
|
173
225
|
<key>StandardErrorPath</key>
|
|
174
|
-
<string>${path.join(
|
|
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(
|
|
204
|
-
StandardError=append:${path.join(
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
console.log("Checking prerequisites...\n");
|
|
300
|
+
const auth = loadAuth();
|
|
233
301
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
//
|
|
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);
|
|
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
|
-
|
|
256
|
-
console.log("
|
|
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
|
-
|
|
260
|
-
console.log(
|
|
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
|
-
|
|
270
|
-
console.log(
|
|
271
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
if (!
|
|
276
|
-
console.log("
|
|
277
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
597
|
+
state.data.workspace = workspace;
|
|
598
|
+
state.completedSteps.push("workspace");
|
|
599
|
+
saveSetupState(state);
|
|
330
600
|
}
|
|
331
601
|
|
|
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, "[]");
|
|
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
|
-
|
|
361
|
-
|
|
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
|
-
//
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|