@locusai/locus-telegram 0.21.11 → 0.21.12

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