@locusai/locus-telegram 0.21.6
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/README.md +117 -0
- package/bin/locus-telegram.js +7 -0
- package/dist/bot.d.ts +7 -0
- package/dist/bot.js +203 -0
- package/dist/commands/git.d.ts +23 -0
- package/dist/commands/git.js +255 -0
- package/dist/commands/locus.d.ts +12 -0
- package/dist/commands/locus.js +169 -0
- package/dist/commands/service.d.ts +6 -0
- package/dist/commands/service.js +58 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +32 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +144 -0
- package/dist/pm2.d.ts +19 -0
- package/dist/pm2.js +112 -0
- package/dist/ui/format.d.ts +30 -0
- package/dist/ui/format.js +100 -0
- package/dist/ui/keyboards.d.ts +37 -0
- package/dist/ui/keyboards.js +73 -0
- package/dist/ui/messages.d.ts +21 -0
- package/dist/ui/messages.js +155 -0
- package/package.json +35 -0
- package/src/bot.ts +256 -0
- package/src/commands/git.ts +309 -0
- package/src/commands/locus.ts +232 -0
- package/src/commands/service.ts +89 -0
- package/src/config.ts +78 -0
- package/src/index.ts +169 -0
- package/src/pm2.ts +138 -0
- package/src/ui/format.ts +130 -0
- package/src/ui/keyboards.ts +83 -0
- package/src/ui/messages.ts +189 -0
- package/tsconfig.json +17 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
}
|
package/src/ui/format.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
|
|
8
|
+
/** Maximum Telegram message length (4096 chars). */
|
|
9
|
+
const MAX_MESSAGE_LENGTH = 4096;
|
|
10
|
+
|
|
11
|
+
/** Maximum length for a code block inside a message. */
|
|
12
|
+
const MAX_CODE_LENGTH = 3800;
|
|
13
|
+
|
|
14
|
+
/** Escape HTML special characters for Telegram HTML mode. */
|
|
15
|
+
export function escapeHtml(text: string): string {
|
|
16
|
+
return text
|
|
17
|
+
.replace(/&/g, "&")
|
|
18
|
+
.replace(/</g, "<")
|
|
19
|
+
.replace(/>/g, ">");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Wrap text in a code block. */
|
|
23
|
+
export function codeBlock(text: string, language = ""): string {
|
|
24
|
+
const truncated = truncate(text, MAX_CODE_LENGTH);
|
|
25
|
+
return `<pre><code${language ? ` class="language-${language}"` : ""}>${escapeHtml(truncated)}</code></pre>`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Wrap text in inline code. */
|
|
29
|
+
export function inlineCode(text: string): string {
|
|
30
|
+
return `<code>${escapeHtml(text)}</code>`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Bold text. */
|
|
34
|
+
export function bold(text: string): string {
|
|
35
|
+
return `<b>${text}</b>`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Italic text. */
|
|
39
|
+
export function italic(text: string): string {
|
|
40
|
+
return `<i>${text}</i>`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Truncate text to a max length, appending "..." if truncated. */
|
|
44
|
+
export function truncate(text: string, maxLength = MAX_MESSAGE_LENGTH): string {
|
|
45
|
+
if (text.length <= maxLength) return text;
|
|
46
|
+
return `${text.slice(0, maxLength - 20)}\n\n... (truncated)`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Split long output into multiple messages if needed. */
|
|
50
|
+
export function splitMessage(text: string): string[] {
|
|
51
|
+
if (text.length <= MAX_MESSAGE_LENGTH) return [text];
|
|
52
|
+
|
|
53
|
+
const chunks: string[] = [];
|
|
54
|
+
let remaining = text;
|
|
55
|
+
|
|
56
|
+
while (remaining.length > 0) {
|
|
57
|
+
if (remaining.length <= MAX_MESSAGE_LENGTH) {
|
|
58
|
+
chunks.push(remaining);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Try to split at a newline
|
|
63
|
+
let splitIdx = remaining.lastIndexOf("\n", MAX_MESSAGE_LENGTH);
|
|
64
|
+
if (splitIdx === -1 || splitIdx < MAX_MESSAGE_LENGTH / 2) {
|
|
65
|
+
splitIdx = MAX_MESSAGE_LENGTH;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
chunks.push(remaining.slice(0, splitIdx));
|
|
69
|
+
remaining = remaining.slice(splitIdx);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return chunks;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Format a command result as a Telegram message. */
|
|
76
|
+
export function formatCommandResult(
|
|
77
|
+
command: string,
|
|
78
|
+
output: string,
|
|
79
|
+
exitCode: number
|
|
80
|
+
): string {
|
|
81
|
+
const status = exitCode === 0 ? "✅" : "❌";
|
|
82
|
+
const header = `${status} ${bold(escapeHtml(command))}`;
|
|
83
|
+
|
|
84
|
+
if (!output.trim()) {
|
|
85
|
+
return exitCode === 0
|
|
86
|
+
? `${header}\n\nCompleted successfully.`
|
|
87
|
+
: `${header}\n\nFailed with exit code ${exitCode}.`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return `${header}\n\n${codeBlock(output.trim())}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Format a streaming progress message. */
|
|
94
|
+
export function formatStreamingMessage(
|
|
95
|
+
command: string,
|
|
96
|
+
output: string,
|
|
97
|
+
isComplete: boolean
|
|
98
|
+
): string {
|
|
99
|
+
const status = isComplete ? "✅" : "⏳";
|
|
100
|
+
const header = `${status} ${bold(escapeHtml(command))}`;
|
|
101
|
+
|
|
102
|
+
if (!output.trim()) {
|
|
103
|
+
return `${header}\n\n${italic("Running...")}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Take last N lines for streaming display
|
|
107
|
+
const lines = output.trim().split("\n");
|
|
108
|
+
const lastLines = lines.slice(-30).join("\n");
|
|
109
|
+
|
|
110
|
+
return `${header}\n\n${codeBlock(lastLines)}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Format an error message. */
|
|
114
|
+
export function formatError(message: string, detail?: string): string {
|
|
115
|
+
let text = `❌ ${bold("Error")}\n\n${escapeHtml(message)}`;
|
|
116
|
+
if (detail) {
|
|
117
|
+
text += `\n\n${codeBlock(detail)}`;
|
|
118
|
+
}
|
|
119
|
+
return text;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Format a success message. */
|
|
123
|
+
export function formatSuccess(message: string): string {
|
|
124
|
+
return `✅ ${escapeHtml(message)}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Format an info message. */
|
|
128
|
+
export function formatInfo(message: string): string {
|
|
129
|
+
return `ℹ️ ${escapeHtml(message)}`;
|
|
130
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline keyboard builders for interactive Telegram responses.
|
|
3
|
+
*
|
|
4
|
+
* Keyboards are attached to messages to let users take actions
|
|
5
|
+
* without typing commands manually.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { InlineKeyboard } from "grammy";
|
|
9
|
+
|
|
10
|
+
// ─── Callback Data Prefixes ─────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export const CB = {
|
|
13
|
+
APPROVE_PLAN: "plan:approve",
|
|
14
|
+
REJECT_PLAN: "plan:reject",
|
|
15
|
+
SHOW_PLAN_DETAILS: "plan:details",
|
|
16
|
+
CONFIRM_ACTION: "confirm:yes",
|
|
17
|
+
CANCEL_ACTION: "confirm:no",
|
|
18
|
+
VIEW_PR: "pr:view:",
|
|
19
|
+
VIEW_LOGS: "logs:view",
|
|
20
|
+
RUN_AGAIN: "run:again:",
|
|
21
|
+
APPROVE_REVIEW: "review:approve:",
|
|
22
|
+
REQUEST_CHANGES: "review:changes:",
|
|
23
|
+
VIEW_DIFF: "review:diff:",
|
|
24
|
+
RUN_SPRINT: "sprint:run",
|
|
25
|
+
VIEW_ISSUES: "issues:view",
|
|
26
|
+
STASH_POP: "stash:pop",
|
|
27
|
+
STASH_LIST: "stash:list",
|
|
28
|
+
STASH_DROP: "stash:drop",
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
// ─── Keyboard Builders ──────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** Keyboard shown after plan creation. */
|
|
34
|
+
export function planKeyboard(): InlineKeyboard {
|
|
35
|
+
return new InlineKeyboard()
|
|
36
|
+
.text("✅ Approve Plan", CB.APPROVE_PLAN)
|
|
37
|
+
.text("❌ Reject Plan", CB.REJECT_PLAN)
|
|
38
|
+
.row()
|
|
39
|
+
.text("📋 Show Details", CB.SHOW_PLAN_DETAILS);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Keyboard shown after a run completes. */
|
|
43
|
+
export function runCompleteKeyboard(issueNumber?: number): InlineKeyboard {
|
|
44
|
+
const kb = new InlineKeyboard();
|
|
45
|
+
kb.text("📄 View Logs", CB.VIEW_LOGS);
|
|
46
|
+
if (issueNumber) {
|
|
47
|
+
kb.text("🔄 Run Again", `${CB.RUN_AGAIN}${issueNumber}`);
|
|
48
|
+
}
|
|
49
|
+
return kb;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Keyboard shown after review. */
|
|
53
|
+
export function reviewKeyboard(prNumber: number): InlineKeyboard {
|
|
54
|
+
return new InlineKeyboard()
|
|
55
|
+
.text("✅ Approve", `${CB.APPROVE_REVIEW}${prNumber}`)
|
|
56
|
+
.text("🔄 Request Changes", `${CB.REQUEST_CHANGES}${prNumber}`)
|
|
57
|
+
.row()
|
|
58
|
+
.text("📝 View Diff", `${CB.VIEW_DIFF}${prNumber}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Confirmation keyboard for destructive operations. */
|
|
62
|
+
export function confirmKeyboard(): InlineKeyboard {
|
|
63
|
+
return new InlineKeyboard()
|
|
64
|
+
.text("✅ Confirm", CB.CONFIRM_ACTION)
|
|
65
|
+
.text("❌ Cancel", CB.CANCEL_ACTION);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Keyboard shown after status check. */
|
|
69
|
+
export function statusKeyboard(): InlineKeyboard {
|
|
70
|
+
return new InlineKeyboard()
|
|
71
|
+
.text("🚀 Run Sprint", CB.RUN_SPRINT)
|
|
72
|
+
.text("📋 Issues", CB.VIEW_ISSUES)
|
|
73
|
+
.row()
|
|
74
|
+
.text("📄 Logs", CB.VIEW_LOGS);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Keyboard for stash operations. */
|
|
78
|
+
export function stashKeyboard(): InlineKeyboard {
|
|
79
|
+
return new InlineKeyboard()
|
|
80
|
+
.text("📤 Pop", CB.STASH_POP)
|
|
81
|
+
.text("📋 List", CB.STASH_LIST)
|
|
82
|
+
.text("🗑 Drop", CB.STASH_DROP);
|
|
83
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Predefined response message templates for common bot interactions.
|
|
3
|
+
*
|
|
4
|
+
* All messages use HTML formatting for Telegram.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { bold, codeBlock, escapeHtml, italic } from "./format.js";
|
|
8
|
+
|
|
9
|
+
// ─── Welcome & Help ─────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export function welcomeMessage(): string {
|
|
12
|
+
return [
|
|
13
|
+
`${bold("🤖 Locus Telegram Bot")}`,
|
|
14
|
+
"",
|
|
15
|
+
"Control your Locus agent directly from Telegram.",
|
|
16
|
+
"",
|
|
17
|
+
`${bold("Locus Commands")}`,
|
|
18
|
+
"/run [issue#...] — Execute issues",
|
|
19
|
+
"/status — Dashboard view",
|
|
20
|
+
"/issues [filters] — List issues",
|
|
21
|
+
"/issue <#> — Show issue details",
|
|
22
|
+
"/sprint [sub] — Sprint management",
|
|
23
|
+
"/plan [args] — AI planning",
|
|
24
|
+
"/review <pr#> — Code review",
|
|
25
|
+
"/iterate <pr#> — Re-execute with feedback",
|
|
26
|
+
"/discuss [topic] — AI discussion",
|
|
27
|
+
"/exec [prompt] — REPL / one-shot",
|
|
28
|
+
"/logs — View logs",
|
|
29
|
+
"/config [path] — View config",
|
|
30
|
+
"/artifacts — View artifacts",
|
|
31
|
+
"",
|
|
32
|
+
`${bold("Git Commands")}`,
|
|
33
|
+
"/gitstatus — Git status",
|
|
34
|
+
"/stage [files|.] — Stage files",
|
|
35
|
+
"/commit <message> — Commit changes",
|
|
36
|
+
"/stash [pop|list|drop] — Stash operations",
|
|
37
|
+
"/branch [name] — List/create branches",
|
|
38
|
+
"/checkout <branch> — Switch branch",
|
|
39
|
+
"/diff — Show diff",
|
|
40
|
+
"/pr <title> — Create pull request",
|
|
41
|
+
"",
|
|
42
|
+
`${bold("Service Commands")}`,
|
|
43
|
+
"/service start|stop|restart|status|logs",
|
|
44
|
+
"",
|
|
45
|
+
"/help — Show this message",
|
|
46
|
+
].join("\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Plan Messages ──────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export function planCreatedMessage(planOutput: string): string {
|
|
52
|
+
return [
|
|
53
|
+
`📋 ${bold("Plan Created")}`,
|
|
54
|
+
"",
|
|
55
|
+
codeBlock(planOutput),
|
|
56
|
+
"",
|
|
57
|
+
italic("Use the buttons below to approve or reject the plan."),
|
|
58
|
+
].join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function planApprovedMessage(): string {
|
|
62
|
+
return `✅ ${bold("Plan Approved")} — Proceeding with execution.`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function planRejectedMessage(): string {
|
|
66
|
+
return `❌ ${bold("Plan Rejected")} — No changes will be made.`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Run Messages ───────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export function runStartedMessage(target: string): string {
|
|
72
|
+
return `🚀 ${bold("Run Started")} — ${escapeHtml(target)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function runCompletedMessage(target: string, prNumber?: number): string {
|
|
76
|
+
const lines = [`✅ ${bold("Run Completed")} — ${escapeHtml(target)}`];
|
|
77
|
+
if (prNumber) {
|
|
78
|
+
lines.push("", `Pull Request: #${prNumber}`);
|
|
79
|
+
}
|
|
80
|
+
return lines.join("\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function runFailedMessage(target: string, error: string): string {
|
|
84
|
+
return [
|
|
85
|
+
`❌ ${bold("Run Failed")} — ${escapeHtml(target)}`,
|
|
86
|
+
"",
|
|
87
|
+
codeBlock(error),
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Review Messages ────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
export function reviewStartedMessage(prNumber: number): string {
|
|
94
|
+
return `🔍 ${bold("Reviewing")} PR #${prNumber}...`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function reviewCompletedMessage(
|
|
98
|
+
prNumber: number,
|
|
99
|
+
output: string
|
|
100
|
+
): string {
|
|
101
|
+
return [
|
|
102
|
+
`✅ ${bold("Review Complete")} — PR #${prNumber}`,
|
|
103
|
+
"",
|
|
104
|
+
codeBlock(output),
|
|
105
|
+
].join("\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Git Messages ───────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export function gitCommitMessage(message: string, hash: string): string {
|
|
111
|
+
return [
|
|
112
|
+
`✅ ${bold("Committed")}`,
|
|
113
|
+
"",
|
|
114
|
+
`Message: ${escapeHtml(message)}`,
|
|
115
|
+
`Hash: ${escapeHtml(hash)}`,
|
|
116
|
+
].join("\n");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function gitBranchCreatedMessage(name: string): string {
|
|
120
|
+
return `✅ Branch ${bold(escapeHtml(name))} created.`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function gitCheckoutMessage(branch: string): string {
|
|
124
|
+
return `✅ Switched to branch ${bold(escapeHtml(branch))}.`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function gitStashMessage(action: string): string {
|
|
128
|
+
return `✅ ${bold("Stash")} — ${escapeHtml(action)} completed.`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function prCreatedMessage(prNumber: number, url: string): string {
|
|
132
|
+
return [
|
|
133
|
+
`✅ ${bold("Pull Request Created")}`,
|
|
134
|
+
"",
|
|
135
|
+
`PR #${prNumber}: ${escapeHtml(url)}`,
|
|
136
|
+
].join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── Service Messages ───────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
export function serviceStatusMessage(
|
|
142
|
+
status: string,
|
|
143
|
+
pid: number | null,
|
|
144
|
+
uptime: number | null,
|
|
145
|
+
memory: number | null,
|
|
146
|
+
restarts: number
|
|
147
|
+
): string {
|
|
148
|
+
const uptimeStr = uptime ? formatUptime(uptime) : "N/A";
|
|
149
|
+
const memoryStr = memory ? formatMemory(memory) : "N/A";
|
|
150
|
+
|
|
151
|
+
return [
|
|
152
|
+
`🤖 ${bold("Locus Telegram Bot")}`,
|
|
153
|
+
"",
|
|
154
|
+
`Status: ${bold(status)}`,
|
|
155
|
+
`PID: ${pid ?? "N/A"}`,
|
|
156
|
+
`Uptime: ${uptimeStr}`,
|
|
157
|
+
`Memory: ${memoryStr}`,
|
|
158
|
+
`Restarts: ${restarts}`,
|
|
159
|
+
].join("\n");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function serviceNotRunningMessage(): string {
|
|
163
|
+
return [
|
|
164
|
+
`⚠️ ${bold("Bot Not Running")}`,
|
|
165
|
+
"",
|
|
166
|
+
"Start the bot with:",
|
|
167
|
+
`${bold("locus pkg telegram start")}`,
|
|
168
|
+
].join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
function formatUptime(startTimeMs: number): string {
|
|
174
|
+
const diff = Date.now() - startTimeMs;
|
|
175
|
+
const seconds = Math.floor(diff / 1000);
|
|
176
|
+
const minutes = Math.floor(seconds / 60);
|
|
177
|
+
const hours = Math.floor(minutes / 60);
|
|
178
|
+
const days = Math.floor(hours / 24);
|
|
179
|
+
|
|
180
|
+
if (days > 0) return `${days}d ${hours % 24}h`;
|
|
181
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
182
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
183
|
+
return `${seconds}s`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function formatMemory(bytes: number): string {
|
|
187
|
+
const mb = bytes / (1024 * 1024);
|
|
188
|
+
return `${mb.toFixed(1)} MB`;
|
|
189
|
+
}
|