@locusai/locus-telegram 0.21.10 → 0.21.11

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.
@@ -1,232 +0,0 @@
1
- /**
2
- * Locus CLI command passthrough — maps every Telegram command to
3
- * the corresponding `locus` CLI invocation via `@locusai/sdk`.
4
- *
5
- * Long-running commands stream output by editing the Telegram message
6
- * in place at regular intervals.
7
- */
8
-
9
- import { invokeLocusStream } from "@locusai/sdk";
10
- import type { Context } from "grammy";
11
- import { formatCommandResult, formatStreamingMessage } from "../ui/format.js";
12
- import {
13
- planKeyboard,
14
- reviewKeyboard,
15
- runCompleteKeyboard,
16
- statusKeyboard,
17
- } from "../ui/keyboards.js";
18
- import { reviewStartedMessage, runStartedMessage } from "../ui/messages.js";
19
-
20
- // ─── Command Map ────────────────────────────────────────────────────────────
21
-
22
- /** Maps Telegram command names to locus CLI arguments. */
23
- const COMMAND_MAP: Record<string, string[]> = {
24
- run: ["run"],
25
- status: ["status"],
26
- issues: ["issue", "list"],
27
- issue: ["issue", "show"],
28
- sprint: ["sprint"],
29
- plan: ["plan"],
30
- review: ["review"],
31
- iterate: ["iterate"],
32
- discuss: ["discuss"],
33
- exec: ["exec"],
34
- logs: ["logs"],
35
- config: ["config"],
36
- artifacts: ["artifacts"],
37
- };
38
-
39
- /** Commands that produce long-running streaming output. */
40
- const STREAMING_COMMANDS = new Set([
41
- "run",
42
- "plan",
43
- "review",
44
- "iterate",
45
- "discuss",
46
- "exec",
47
- ]);
48
-
49
- /** Minimum interval between message edits (ms) to avoid Telegram rate limits. */
50
- const EDIT_INTERVAL = 2000;
51
-
52
- // ─── Handlers ───────────────────────────────────────────────────────────────
53
-
54
- /**
55
- * Execute a locus CLI command, streaming output to the Telegram chat.
56
- */
57
- export async function handleLocusCommand(
58
- ctx: Context,
59
- command: string,
60
- args: string[]
61
- ): Promise<void> {
62
- const cliArgs = COMMAND_MAP[command];
63
- if (!cliArgs) {
64
- await ctx.reply(`Unknown command: /${command}`);
65
- return;
66
- }
67
-
68
- const fullArgs = [...cliArgs, ...args];
69
- const displayCmd = `locus ${fullArgs.join(" ")}`;
70
- const isStreaming = STREAMING_COMMANDS.has(command);
71
-
72
- // Send pre-messages for specific commands
73
- if (command === "run" && args.length > 0) {
74
- await ctx.reply(runStartedMessage(args.join(" ")), { parse_mode: "HTML" });
75
- } else if (command === "review" && args.length > 0) {
76
- await ctx.reply(reviewStartedMessage(Number(args[0])), {
77
- parse_mode: "HTML",
78
- });
79
- }
80
-
81
- if (isStreaming) {
82
- await handleStreamingCommand(ctx, displayCmd, fullArgs, command, args);
83
- } else {
84
- await handleBufferedCommand(ctx, displayCmd, fullArgs, command);
85
- }
86
- }
87
-
88
- /**
89
- * Handle a streaming command — edit the message in place as output arrives.
90
- */
91
- async function handleStreamingCommand(
92
- ctx: Context,
93
- displayCmd: string,
94
- fullArgs: string[],
95
- command: string,
96
- args: string[]
97
- ): Promise<void> {
98
- const child = invokeLocusStream(fullArgs);
99
-
100
- let output = "";
101
- let lastEditTime = 0;
102
- let editTimer: ReturnType<typeof setTimeout> | null = null;
103
-
104
- // Send initial "running" message
105
- const msg = await ctx.reply(formatStreamingMessage(displayCmd, "", false), {
106
- parse_mode: "HTML",
107
- });
108
-
109
- const editMessage = async () => {
110
- const now = Date.now();
111
- if (now - lastEditTime < EDIT_INTERVAL) return;
112
- lastEditTime = now;
113
-
114
- try {
115
- await ctx.api.editMessageText(
116
- msg.chat.id,
117
- msg.message_id,
118
- formatStreamingMessage(displayCmd, output, false),
119
- { parse_mode: "HTML" }
120
- );
121
- } catch {
122
- // Edit can fail if content hasn't changed — ignore
123
- }
124
- };
125
-
126
- child.stdout?.on("data", (chunk: Buffer) => {
127
- output += chunk.toString();
128
-
129
- // Debounce edits
130
- if (editTimer) clearTimeout(editTimer);
131
- editTimer = setTimeout(editMessage, EDIT_INTERVAL);
132
- });
133
-
134
- child.stderr?.on("data", (chunk: Buffer) => {
135
- output += chunk.toString();
136
- });
137
-
138
- await new Promise<void>((resolve) => {
139
- child.on("close", async (exitCode) => {
140
- if (editTimer) clearTimeout(editTimer);
141
-
142
- // Final edit with complete output
143
- try {
144
- await ctx.api.editMessageText(
145
- msg.chat.id,
146
- msg.message_id,
147
- formatStreamingMessage(displayCmd, output, true),
148
- { parse_mode: "HTML" }
149
- );
150
- } catch {
151
- // ignore edit failures
152
- }
153
-
154
- // Send keyboard for post-command actions
155
- const keyboard = getPostCommandKeyboard(command, args, exitCode ?? 0);
156
- if (keyboard) {
157
- await ctx.reply(
158
- exitCode === 0
159
- ? "What would you like to do next?"
160
- : "Command failed.",
161
- { reply_markup: keyboard }
162
- );
163
- }
164
-
165
- resolve();
166
- });
167
- });
168
- }
169
-
170
- /**
171
- * Handle a buffered command — collect all output, then send once.
172
- */
173
- async function handleBufferedCommand(
174
- ctx: Context,
175
- displayCmd: string,
176
- fullArgs: string[],
177
- command: string
178
- ): Promise<void> {
179
- const child = invokeLocusStream(fullArgs);
180
- let output = "";
181
-
182
- child.stdout?.on("data", (chunk: Buffer) => {
183
- output += chunk.toString();
184
- });
185
-
186
- child.stderr?.on("data", (chunk: Buffer) => {
187
- output += chunk.toString();
188
- });
189
-
190
- await new Promise<void>((resolve) => {
191
- child.on("close", async (exitCode) => {
192
- const result = formatCommandResult(displayCmd, output, exitCode ?? 0);
193
-
194
- const keyboard = getPostCommandKeyboard(command, [], exitCode ?? 0);
195
- await ctx.reply(result, {
196
- parse_mode: "HTML",
197
- reply_markup: keyboard ?? undefined,
198
- });
199
-
200
- resolve();
201
- });
202
- });
203
- }
204
-
205
- // ─── Post-Command Keyboards ────────────────────────────────────────────────
206
-
207
- function getPostCommandKeyboard(
208
- command: string,
209
- args: string[],
210
- exitCode: number
211
- ) {
212
- if (exitCode !== 0) return null;
213
-
214
- switch (command) {
215
- case "plan":
216
- return planKeyboard();
217
- case "review":
218
- if (args.length > 0) {
219
- return reviewKeyboard(Number(args[0]));
220
- }
221
- return null;
222
- case "run":
223
- if (args.length > 0) {
224
- return runCompleteKeyboard(Number(args[0]));
225
- }
226
- return runCompleteKeyboard();
227
- case "status":
228
- return statusKeyboard();
229
- default:
230
- return null;
231
- }
232
- }
@@ -1,89 +0,0 @@
1
- /**
2
- * Service management commands — PM2 lifecycle control via Telegram.
3
- */
4
-
5
- import type { Context } from "grammy";
6
- import {
7
- pm2Delete,
8
- pm2Logs,
9
- pm2Restart,
10
- pm2Start,
11
- pm2Status,
12
- pm2Stop,
13
- } from "../pm2.js";
14
- import { codeBlock, formatError, formatSuccess } from "../ui/format.js";
15
- import {
16
- serviceNotRunningMessage,
17
- serviceStatusMessage,
18
- } from "../ui/messages.js";
19
-
20
- /** /service <subcommand> — manage the bot process */
21
- export async function handleService(
22
- ctx: Context,
23
- args: string[]
24
- ): Promise<void> {
25
- const subcommand = args[0] ?? "status";
26
-
27
- try {
28
- switch (subcommand) {
29
- case "start": {
30
- const result = pm2Start();
31
- await ctx.reply(formatSuccess(result), { parse_mode: "HTML" });
32
- break;
33
- }
34
- case "stop": {
35
- const result = pm2Stop();
36
- await ctx.reply(formatSuccess(result), { parse_mode: "HTML" });
37
- break;
38
- }
39
- case "restart": {
40
- const result = pm2Restart();
41
- await ctx.reply(formatSuccess(result), { parse_mode: "HTML" });
42
- break;
43
- }
44
- case "delete": {
45
- const result = pm2Delete();
46
- await ctx.reply(formatSuccess(result), { parse_mode: "HTML" });
47
- break;
48
- }
49
- case "status": {
50
- const status = pm2Status();
51
- if (!status) {
52
- await ctx.reply(serviceNotRunningMessage(), {
53
- parse_mode: "HTML",
54
- });
55
- } else {
56
- await ctx.reply(
57
- serviceStatusMessage(
58
- status.status,
59
- status.pid,
60
- status.uptime,
61
- status.memory,
62
- status.restarts
63
- ),
64
- { parse_mode: "HTML" }
65
- );
66
- }
67
- break;
68
- }
69
- case "logs": {
70
- const lines = args[1] ? Number(args[1]) : 50;
71
- const logs = pm2Logs(lines);
72
- await ctx.reply(codeBlock(logs), { parse_mode: "HTML" });
73
- break;
74
- }
75
- default: {
76
- await ctx.reply(
77
- formatError(
78
- "Unknown service command. Use: start, stop, restart, delete, status, logs"
79
- ),
80
- { parse_mode: "HTML" }
81
- );
82
- }
83
- }
84
- } catch (error: unknown) {
85
- await ctx.reply(formatError("Service command failed", String(error)), {
86
- parse_mode: "HTML",
87
- });
88
- }
89
- }
package/src/config.ts DELETED
@@ -1,78 +0,0 @@
1
- /**
2
- * Configuration for the Telegram bot, read from the Locus config system.
3
- *
4
- * Users configure via:
5
- * locus config packages.telegram.botToken "123456:ABC..."
6
- * locus config packages.telegram.chatIds "12345678,87654321"
7
- */
8
-
9
- import { readLocusConfig } from "@locusai/sdk";
10
-
11
- export interface TelegramConfig {
12
- botToken: string;
13
- allowedChatIds: number[];
14
- }
15
-
16
- export function loadTelegramConfig(): TelegramConfig {
17
- const locusConfig = readLocusConfig();
18
- const pkg = locusConfig.packages?.telegram;
19
-
20
- const botToken = pkg?.botToken;
21
- if (!botToken || typeof botToken !== "string") {
22
- throw new Error(
23
- 'Telegram bot token not configured. Run:\n locus config packages.telegram.botToken "<your-token>"\n\nGet a token from @BotFather on Telegram.'
24
- );
25
- }
26
-
27
- const chatIdsRaw = pkg?.chatIds;
28
- if (!chatIdsRaw) {
29
- throw new Error(
30
- 'Telegram chat IDs not configured. Run:\n locus config packages.telegram.chatIds "12345678"\n\nSend /start to your bot, then use the chat ID from the Telegram API.'
31
- );
32
- }
33
-
34
- const allowedChatIds = parseChatIds(chatIdsRaw);
35
-
36
- if (allowedChatIds.length === 0) {
37
- throw new Error(
38
- "packages.telegram.chatIds must contain at least one chat ID."
39
- );
40
- }
41
-
42
- return { botToken, allowedChatIds };
43
- }
44
-
45
- function parseChatIds(raw: unknown): number[] {
46
- // Support both array of numbers and comma-separated string
47
- if (Array.isArray(raw)) {
48
- return raw.map((id) => {
49
- const parsed = Number(id);
50
- if (Number.isNaN(parsed)) {
51
- throw new Error(`Invalid chat ID: "${id}". Must be a number.`);
52
- }
53
- return parsed;
54
- });
55
- }
56
-
57
- if (typeof raw === "string") {
58
- return raw
59
- .split(",")
60
- .map((id) => id.trim())
61
- .filter((id) => id.length > 0)
62
- .map((id) => {
63
- const parsed = Number.parseInt(id, 10);
64
- if (Number.isNaN(parsed)) {
65
- throw new Error(`Invalid chat ID: "${id}". Must be a number.`);
66
- }
67
- return parsed;
68
- });
69
- }
70
-
71
- if (typeof raw === "number") {
72
- return [raw];
73
- }
74
-
75
- throw new Error(
76
- "Invalid chatIds format. Expected a number, array of numbers, or comma-separated string."
77
- );
78
- }
package/src/index.ts DELETED
@@ -1,169 +0,0 @@
1
- /**
2
- * Main entry point for locus-telegram.
3
- *
4
- * Dispatches to either:
5
- * - PM2 service management (start/stop/restart/status/logs)
6
- * - Direct bot execution (bot)
7
- *
8
- * Usage:
9
- * locus pkg telegram start → start bot via PM2
10
- * locus pkg telegram stop → stop bot via PM2
11
- * locus pkg telegram restart → restart bot
12
- * locus pkg telegram status → show PM2 process status
13
- * locus pkg telegram logs → tail PM2 logs
14
- * locus pkg telegram bot → run bot directly (foreground)
15
- */
16
-
17
- import { createLogger } from "@locusai/sdk";
18
- import { createBot } from "./bot.js";
19
- import { loadTelegramConfig } from "./config.js";
20
- import {
21
- pm2Delete,
22
- pm2Logs,
23
- pm2Restart,
24
- pm2Start,
25
- pm2Status,
26
- pm2Stop,
27
- } from "./pm2.js";
28
-
29
- const logger = createLogger("telegram");
30
-
31
- export async function main(args: string[]): Promise<void> {
32
- const command = args[0] ?? "help";
33
-
34
- switch (command) {
35
- case "start":
36
- return handleStart();
37
- case "stop":
38
- return handleStop();
39
- case "restart":
40
- return handleRestart();
41
- case "delete":
42
- return handleDelete();
43
- case "status":
44
- return handleStatus();
45
- case "logs":
46
- return handleLogs(args.slice(1));
47
- case "bot":
48
- return handleBot();
49
- case "help":
50
- case "--help":
51
- case "-h":
52
- return printHelp();
53
- default:
54
- console.error(`Unknown command: ${command}`);
55
- printHelp();
56
- process.exit(1);
57
- }
58
- }
59
-
60
- // ─── PM2 Service Commands ───────────────────────────────────────────────────
61
-
62
- function handleStart(): void {
63
- const result = pm2Start();
64
- logger.info(result);
65
- }
66
-
67
- function handleStop(): void {
68
- const result = pm2Stop();
69
- logger.info(result);
70
- }
71
-
72
- function handleRestart(): void {
73
- const result = pm2Restart();
74
- logger.info(result);
75
- }
76
-
77
- function handleDelete(): void {
78
- const result = pm2Delete();
79
- logger.info(result);
80
- }
81
-
82
- function handleStatus(): void {
83
- const status = pm2Status();
84
- if (!status) {
85
- logger.warn("Bot is not running");
86
- return;
87
- }
88
-
89
- const uptimeStr = status.uptime
90
- ? `${Math.floor((Date.now() - status.uptime) / 1000)}s`
91
- : "N/A";
92
- const memStr = status.memory
93
- ? `${(status.memory / (1024 * 1024)).toFixed(1)}MB`
94
- : "N/A";
95
-
96
- console.log(`
97
- Name: ${status.name}
98
- Status: ${status.status}
99
- PID: ${status.pid ?? "N/A"}
100
- Uptime: ${uptimeStr}
101
- Memory: ${memStr}
102
- Restarts: ${status.restarts}
103
- `);
104
- }
105
-
106
- function handleLogs(args: string[]): void {
107
- const lines = args[0] ? Number(args[0]) : 50;
108
- const logs = pm2Logs(lines);
109
- console.log(logs);
110
- }
111
-
112
- // ─── Direct Bot Execution ───────────────────────────────────────────────────
113
-
114
- async function handleBot(): Promise<void> {
115
- const config = loadTelegramConfig();
116
- const bot = createBot(config);
117
-
118
- logger.info("Starting Telegram bot...");
119
- logger.info(`Allowed chat IDs: ${config.allowedChatIds.join(", ")}`);
120
-
121
- // Graceful shutdown
122
- const shutdown = () => {
123
- logger.info("Shutting down bot...");
124
- bot.stop();
125
- process.exit(0);
126
- };
127
-
128
- process.on("SIGINT", shutdown);
129
- process.on("SIGTERM", shutdown);
130
-
131
- await bot.start({
132
- onStart: () => {
133
- logger.info("Bot is running. Send /help in Telegram to get started.");
134
- },
135
- });
136
- }
137
-
138
- // ─── Help ───────────────────────────────────────────────────────────────────
139
-
140
- function printHelp(): void {
141
- console.log(`
142
- locus-telegram — Remote-control Locus via Telegram
143
-
144
- Usage:
145
- locus pkg telegram <command>
146
-
147
- Commands:
148
- start Start the bot via PM2 (background)
149
- stop Stop the bot
150
- restart Restart the bot
151
- delete Remove the bot from PM2
152
- status Show bot process status
153
- logs [n] Show last n lines of logs (default: 50)
154
- bot Run the bot directly (foreground)
155
- help Show this help message
156
-
157
- Configuration (via locus config):
158
- packages.telegram.botToken Telegram Bot API token (from @BotFather)
159
- packages.telegram.chatIds Comma-separated chat IDs or JSON array
160
-
161
- Setup:
162
- locus config packages.telegram.botToken "123456:ABC-DEF..."
163
- locus config packages.telegram.chatIds "12345678"
164
-
165
- Examples:
166
- locus pkg telegram start # Start in background
167
- locus pkg telegram bot # Run in foreground (development)
168
- `);
169
- }
package/src/pm2.ts DELETED
@@ -1,138 +0,0 @@
1
- /**
2
- * PM2 programmatic wrapper for managing the Telegram bot process.
3
- *
4
- * Uses pm2 package API to start/stop/restart/status/logs the bot.
5
- */
6
-
7
- import { execSync } from "node:child_process";
8
- import { dirname, join } from "node:path";
9
- import { fileURLToPath } from "node:url";
10
-
11
- const PROCESS_NAME = "locus-telegram";
12
-
13
- function getPm2Bin(): string {
14
- // Try local node_modules first, then global
15
- try {
16
- const result = execSync("which pm2", {
17
- encoding: "utf-8",
18
- stdio: ["pipe", "pipe", "pipe"],
19
- }).trim();
20
- if (result) return result;
21
- } catch {
22
- // fall through
23
- }
24
-
25
- // Try npx as fallback
26
- return "npx pm2";
27
- }
28
-
29
- function pm2Exec(args: string): string {
30
- const pm2 = getPm2Bin();
31
- try {
32
- return execSync(`${pm2} ${args}`, {
33
- encoding: "utf-8",
34
- stdio: ["pipe", "pipe", "pipe"],
35
- env: process.env,
36
- });
37
- } catch (error: unknown) {
38
- const err = error as { stderr?: string; message?: string };
39
- throw new Error(err.stderr?.trim() || err.message || "PM2 command failed");
40
- }
41
- }
42
-
43
- function getBotScriptPath(): string {
44
- const currentFile = fileURLToPath(import.meta.url);
45
- const packageRoot = dirname(dirname(currentFile));
46
- return join(packageRoot, "bin", "locus-telegram.js");
47
- }
48
-
49
- export function pm2Start(): string {
50
- const script = getBotScriptPath();
51
- const pm2 = getPm2Bin();
52
-
53
- try {
54
- // Check if already running
55
- const list = pm2Exec("jlist");
56
- const processes = JSON.parse(list) as Array<{ name: string }>;
57
- const existing = processes.find((p) => p.name === PROCESS_NAME);
58
- if (existing) {
59
- pm2Exec(`restart ${PROCESS_NAME}`);
60
- return `Restarted ${PROCESS_NAME}`;
61
- }
62
- } catch {
63
- // Not running, start fresh
64
- }
65
-
66
- execSync(
67
- `${pm2} start ${JSON.stringify(script)} --name ${PROCESS_NAME} -- bot`,
68
- {
69
- encoding: "utf-8",
70
- stdio: "inherit",
71
- env: process.env,
72
- }
73
- );
74
-
75
- return `Started ${PROCESS_NAME}`;
76
- }
77
-
78
- export function pm2Stop(): string {
79
- pm2Exec(`stop ${PROCESS_NAME}`);
80
- return `Stopped ${PROCESS_NAME}`;
81
- }
82
-
83
- export function pm2Restart(): string {
84
- pm2Exec(`restart ${PROCESS_NAME}`);
85
- return `Restarted ${PROCESS_NAME}`;
86
- }
87
-
88
- export function pm2Delete(): string {
89
- pm2Exec(`delete ${PROCESS_NAME}`);
90
- return `Deleted ${PROCESS_NAME}`;
91
- }
92
-
93
- export interface Pm2Status {
94
- name: string;
95
- status: string;
96
- pid: number | null;
97
- uptime: number | null;
98
- memory: number | null;
99
- restarts: number;
100
- }
101
-
102
- export function pm2Status(): Pm2Status | null {
103
- try {
104
- const list = pm2Exec("jlist");
105
- const processes = JSON.parse(list) as Array<{
106
- name: string;
107
- pm2_env?: {
108
- status?: string;
109
- pm_uptime?: number;
110
- restart_time?: number;
111
- };
112
- pid?: number;
113
- monit?: { memory?: number };
114
- }>;
115
-
116
- const proc = processes.find((p) => p.name === PROCESS_NAME);
117
- if (!proc) return null;
118
-
119
- return {
120
- name: PROCESS_NAME,
121
- status: proc.pm2_env?.status ?? "unknown",
122
- pid: proc.pid ?? null,
123
- uptime: proc.pm2_env?.pm_uptime ?? null,
124
- memory: proc.monit?.memory ?? null,
125
- restarts: proc.pm2_env?.restart_time ?? 0,
126
- };
127
- } catch {
128
- return null;
129
- }
130
- }
131
-
132
- export function pm2Logs(lines = 50): string {
133
- try {
134
- return pm2Exec(`logs ${PROCESS_NAME} --nostream --lines ${lines}`);
135
- } catch {
136
- return "No logs available.";
137
- }
138
- }