@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 ADDED
@@ -0,0 +1,117 @@
1
+ # locus-telegram
2
+
3
+ Remote-control your [Locus](https://github.com/locusai/locus) agent from Telegram. Full CLI command mapping, git operations, interactive keyboards, and built-in PM2 process management.
4
+
5
+ ## Setup
6
+
7
+ ### 1. Create a Telegram Bot
8
+
9
+ 1. Open [@BotFather](https://t.me/BotFather) on Telegram
10
+ 2. Send `/newbot` and follow the prompts
11
+ 3. Copy the bot token
12
+
13
+ ### 2. Get Your Chat ID
14
+
15
+ 1. Send any message to your new bot
16
+ 2. Visit `https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates`
17
+ 3. Find `"chat":{"id": <YOUR_CHAT_ID>}` in the response
18
+
19
+ ### 3. Configure Locus
20
+
21
+ ```sh
22
+ locus config packages.telegram.botToken "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
23
+ locus config packages.telegram.chatIds "12345678"
24
+ ```
25
+
26
+ Multiple chat IDs can be comma-separated: `"12345678,87654321"`
27
+
28
+ ### 4. Install & Start
29
+
30
+ ```sh
31
+ locus install locus-telegram
32
+ locus pkg telegram start
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ### Service Management (PM2)
38
+
39
+ ```sh
40
+ locus pkg telegram start # Start bot in background via PM2
41
+ locus pkg telegram stop # Stop the bot
42
+ locus pkg telegram restart # Restart the bot
43
+ locus pkg telegram status # Show process status
44
+ locus pkg telegram logs [n] # Show last n lines of logs
45
+ locus pkg telegram bot # Run in foreground (development)
46
+ ```
47
+
48
+ ### Telegram Commands
49
+
50
+ #### Locus CLI
51
+
52
+ | Command | Description |
53
+ |---------|-------------|
54
+ | `/run [issue#...]` | Execute issues |
55
+ | `/status` | Dashboard view |
56
+ | `/issues` | List issues |
57
+ | `/issue <#>` | Show issue details |
58
+ | `/sprint [sub]` | Sprint management |
59
+ | `/plan [args]` | AI planning |
60
+ | `/review <pr#>` | Code review |
61
+ | `/iterate <pr#>` | Re-execute with feedback |
62
+ | `/discuss [topic]` | AI discussion |
63
+ | `/exec [prompt]` | REPL / one-shot |
64
+ | `/logs` | View logs |
65
+ | `/config [path]` | View config |
66
+ | `/artifacts` | View artifacts |
67
+
68
+ #### Git Operations
69
+
70
+ | Command | Description |
71
+ |---------|-------------|
72
+ | `/gitstatus` | Git status |
73
+ | `/stage [files\|.]` | Stage files |
74
+ | `/commit <message>` | Commit changes |
75
+ | `/stash [pop\|list\|drop]` | Stash operations |
76
+ | `/branch [name]` | List/create branches |
77
+ | `/checkout <branch>` | Switch branch |
78
+ | `/diff` | Show diff |
79
+ | `/pr <title>` | Create pull request |
80
+
81
+ #### Service
82
+
83
+ | Command | Description |
84
+ |---------|-------------|
85
+ | `/service start\|stop\|restart\|status\|logs` | Manage bot process |
86
+
87
+ ### Interactive Features
88
+
89
+ The bot automatically shows inline keyboard buttons after certain commands:
90
+
91
+ - **After `/plan`** — Approve, Reject, or Show Details
92
+ - **After `/run`** — View Logs, Run Again
93
+ - **After `/review`** — Approve, Request Changes, View Diff
94
+ - **After `/status`** — Run Sprint, View Issues, View Logs
95
+ - **After `/stash`** — Pop, List, Drop
96
+
97
+ Non-command text messages are automatically sent to `locus exec` as prompts.
98
+
99
+ ## Security
100
+
101
+ Only chat IDs listed in `packages.telegram.chatIds` in the locus config can interact with the bot. All other messages are silently ignored.
102
+
103
+ ## Development
104
+
105
+ ```sh
106
+ cd packages/telegram
107
+ bun install
108
+ bun run build # Compile TypeScript
109
+ bun run typecheck # Type-check only
110
+
111
+ # Configure (if not already done)
112
+ locus config packages.telegram.botToken "..."
113
+ locus config packages.telegram.chatIds "..."
114
+
115
+ # Run in foreground for development
116
+ locus pkg telegram bot
117
+ ```
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../dist/index.js";
3
+
4
+ main(process.argv.slice(2)).catch((err) => {
5
+ console.error(err.message || err);
6
+ process.exit(1);
7
+ });
package/dist/bot.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Bot instance — sets up grammy bot, registers middleware, commands,
3
+ * and callback query handlers.
4
+ */
5
+ import { Bot } from "grammy";
6
+ import type { TelegramConfig } from "./config.js";
7
+ export declare function createBot(config: TelegramConfig): Bot;
package/dist/bot.js ADDED
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Bot instance — sets up grammy bot, registers middleware, commands,
3
+ * and callback query handlers.
4
+ */
5
+ import { Bot } from "grammy";
6
+ import { createLogger } from "@locusai/sdk";
7
+ import { handleLocusCommand } from "./commands/locus.js";
8
+ import { handleGitStatus, handleStage, handleCommit, handleStash, handleBranch, handleCheckout, handleDiff, handlePR, } from "./commands/git.js";
9
+ import { handleService } from "./commands/service.js";
10
+ import { CB } from "./ui/keyboards.js";
11
+ import { welcomeMessage } from "./ui/messages.js";
12
+ import { formatSuccess, formatInfo } from "./ui/format.js";
13
+ const logger = createLogger("telegram");
14
+ // ─── Bot Factory ────────────────────────────────────────────────────────────
15
+ export function createBot(config) {
16
+ const bot = new Bot(config.botToken);
17
+ // ── Auth Middleware ─────────────────────────────────────────────────────
18
+ bot.use(async (ctx, next) => {
19
+ const chatId = ctx.chat?.id;
20
+ if (!chatId || !config.allowedChatIds.includes(chatId)) {
21
+ logger.warn("Unauthorized access attempt", {
22
+ chatId,
23
+ from: ctx.from?.username,
24
+ });
25
+ return; // silently ignore
26
+ }
27
+ await next();
28
+ });
29
+ // ── Help / Start ────────────────────────────────────────────────────────
30
+ bot.command("start", async (ctx) => {
31
+ await ctx.reply(welcomeMessage(), { parse_mode: "HTML" });
32
+ });
33
+ bot.command("help", async (ctx) => {
34
+ await ctx.reply(welcomeMessage(), { parse_mode: "HTML" });
35
+ });
36
+ // ── Locus CLI Commands ──────────────────────────────────────────────────
37
+ const locusCommands = [
38
+ "run",
39
+ "status",
40
+ "issues",
41
+ "issue",
42
+ "sprint",
43
+ "plan",
44
+ "review",
45
+ "iterate",
46
+ "discuss",
47
+ "exec",
48
+ "logs",
49
+ "config",
50
+ "artifacts",
51
+ ];
52
+ for (const cmd of locusCommands) {
53
+ bot.command(cmd, async (ctx) => {
54
+ const args = parseArgs(ctx.message?.text ?? "", cmd);
55
+ await handleLocusCommand(ctx, cmd, args);
56
+ });
57
+ }
58
+ // ── Git Commands ────────────────────────────────────────────────────────
59
+ bot.command("gitstatus", async (ctx) => {
60
+ await handleGitStatus(ctx);
61
+ });
62
+ bot.command("stage", async (ctx) => {
63
+ const args = parseArgs(ctx.message?.text ?? "", "stage");
64
+ await handleStage(ctx, args);
65
+ });
66
+ bot.command("commit", async (ctx) => {
67
+ const args = parseArgs(ctx.message?.text ?? "", "commit");
68
+ await handleCommit(ctx, args);
69
+ });
70
+ bot.command("stash", async (ctx) => {
71
+ const args = parseArgs(ctx.message?.text ?? "", "stash");
72
+ await handleStash(ctx, args);
73
+ });
74
+ bot.command("branch", async (ctx) => {
75
+ const args = parseArgs(ctx.message?.text ?? "", "branch");
76
+ await handleBranch(ctx, args);
77
+ });
78
+ bot.command("checkout", async (ctx) => {
79
+ const args = parseArgs(ctx.message?.text ?? "", "checkout");
80
+ await handleCheckout(ctx, args);
81
+ });
82
+ bot.command("diff", async (ctx) => {
83
+ await handleDiff(ctx);
84
+ });
85
+ bot.command("pr", async (ctx) => {
86
+ const args = parseArgs(ctx.message?.text ?? "", "pr");
87
+ await handlePR(ctx, args);
88
+ });
89
+ // ── Service Commands ────────────────────────────────────────────────────
90
+ bot.command("service", async (ctx) => {
91
+ const args = parseArgs(ctx.message?.text ?? "", "service");
92
+ await handleService(ctx, args);
93
+ });
94
+ // ── Callback Query Handlers ─────────────────────────────────────────────
95
+ registerCallbackHandlers(bot);
96
+ // ── Fallback ────────────────────────────────────────────────────────────
97
+ bot.on("message:text", async (ctx) => {
98
+ // Treat non-command text as a locus exec prompt
99
+ const text = ctx.message.text;
100
+ if (text.startsWith("/"))
101
+ return; // unknown command — ignore
102
+ await handleLocusCommand(ctx, "exec", [text]);
103
+ });
104
+ return bot;
105
+ }
106
+ // ─── Callback Query Handlers ────────────────────────────────────────────────
107
+ function registerCallbackHandlers(bot) {
108
+ // Plan callbacks
109
+ bot.callbackQuery(CB.APPROVE_PLAN, async (ctx) => {
110
+ await ctx.answerCallbackQuery({ text: "Plan approved!" });
111
+ await ctx.editMessageReplyMarkup({ reply_markup: undefined });
112
+ await ctx.reply(formatSuccess("Plan approved. Starting execution..."), {
113
+ parse_mode: "HTML",
114
+ });
115
+ // Trigger the run
116
+ await handleLocusCommand(ctx, "run", []);
117
+ });
118
+ bot.callbackQuery(CB.REJECT_PLAN, async (ctx) => {
119
+ await ctx.answerCallbackQuery({ text: "Plan rejected." });
120
+ await ctx.editMessageReplyMarkup({ reply_markup: undefined });
121
+ await ctx.reply(formatInfo("Plan rejected. No changes will be made."), {
122
+ parse_mode: "HTML",
123
+ });
124
+ });
125
+ bot.callbackQuery(CB.SHOW_PLAN_DETAILS, async (ctx) => {
126
+ await ctx.answerCallbackQuery();
127
+ await handleLocusCommand(ctx, "plan", ["--show"]);
128
+ });
129
+ // Run callbacks
130
+ bot.callbackQuery(CB.VIEW_LOGS, async (ctx) => {
131
+ await ctx.answerCallbackQuery();
132
+ await handleLocusCommand(ctx, "logs", ["--lines", "30"]);
133
+ });
134
+ bot.callbackQuery(/^run:again:(\d+)$/, async (ctx) => {
135
+ const issue = ctx.match[1];
136
+ await ctx.answerCallbackQuery({ text: `Re-running issue #${issue}` });
137
+ await ctx.editMessageReplyMarkup({ reply_markup: undefined });
138
+ await handleLocusCommand(ctx, "run", [issue]);
139
+ });
140
+ // Review callbacks
141
+ bot.callbackQuery(/^review:approve:(\d+)$/, async (ctx) => {
142
+ const pr = ctx.match[1];
143
+ await ctx.answerCallbackQuery({ text: "Approving PR..." });
144
+ await ctx.editMessageReplyMarkup({ reply_markup: undefined });
145
+ await ctx.reply(formatSuccess(`Approved PR #${pr}`), {
146
+ parse_mode: "HTML",
147
+ });
148
+ });
149
+ bot.callbackQuery(/^review:changes:(\d+)$/, async (ctx) => {
150
+ const pr = ctx.match[1];
151
+ await ctx.answerCallbackQuery();
152
+ await ctx.editMessageReplyMarkup({ reply_markup: undefined });
153
+ await ctx.reply(formatInfo(`Requesting changes on PR #${pr}. Reply with your feedback and I'll pass it along via /iterate ${pr}`), { parse_mode: "HTML" });
154
+ });
155
+ bot.callbackQuery(/^review:diff:(\d+)$/, async (ctx) => {
156
+ const pr = ctx.match[1];
157
+ await ctx.answerCallbackQuery();
158
+ await handleLocusCommand(ctx, "review", [pr, "--diff"]);
159
+ });
160
+ // Status callbacks
161
+ bot.callbackQuery(CB.RUN_SPRINT, async (ctx) => {
162
+ await ctx.answerCallbackQuery({ text: "Starting sprint run..." });
163
+ await ctx.editMessageReplyMarkup({ reply_markup: undefined });
164
+ await handleLocusCommand(ctx, "run", []);
165
+ });
166
+ bot.callbackQuery(CB.VIEW_ISSUES, async (ctx) => {
167
+ await ctx.answerCallbackQuery();
168
+ await handleLocusCommand(ctx, "issues", []);
169
+ });
170
+ // Stash callbacks
171
+ bot.callbackQuery(CB.STASH_POP, async (ctx) => {
172
+ await ctx.answerCallbackQuery();
173
+ await handleStash(ctx, ["pop"]);
174
+ });
175
+ bot.callbackQuery(CB.STASH_LIST, async (ctx) => {
176
+ await ctx.answerCallbackQuery();
177
+ await handleStash(ctx, ["list"]);
178
+ });
179
+ bot.callbackQuery(CB.STASH_DROP, async (ctx) => {
180
+ await ctx.answerCallbackQuery();
181
+ await handleStash(ctx, ["drop"]);
182
+ });
183
+ // Confirmation callbacks
184
+ bot.callbackQuery(CB.CONFIRM_ACTION, async (ctx) => {
185
+ await ctx.answerCallbackQuery({ text: "Confirmed!" });
186
+ await ctx.editMessageReplyMarkup({ reply_markup: undefined });
187
+ });
188
+ bot.callbackQuery(CB.CANCEL_ACTION, async (ctx) => {
189
+ await ctx.answerCallbackQuery({ text: "Cancelled." });
190
+ await ctx.editMessageReplyMarkup({ reply_markup: undefined });
191
+ await ctx.reply(formatInfo("Action cancelled."), { parse_mode: "HTML" });
192
+ });
193
+ }
194
+ // ─── Helpers ────────────────────────────────────────────────────────────────
195
+ /** Parse arguments from a Telegram command message. */
196
+ function parseArgs(text, command) {
197
+ // Remove the /command part (handles @botname suffix too)
198
+ const prefixRegex = new RegExp(`^/${command}(@\\S+)?\\s*`);
199
+ const rest = text.replace(prefixRegex, "").trim();
200
+ if (!rest)
201
+ return [];
202
+ return rest.split(/\s+/);
203
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Git command handlers — stage, commit, stash, branch, checkout, diff, PR.
3
+ *
4
+ * Uses direct git subprocess calls for fast, local operations.
5
+ * PR creation uses `gh` CLI (same as the locus CLI does).
6
+ */
7
+ import type { Context } from "grammy";
8
+ /** /gitstatus — show git status */
9
+ export declare function handleGitStatus(ctx: Context): Promise<void>;
10
+ /** /stage [files|.] — stage files for commit */
11
+ export declare function handleStage(ctx: Context, args: string[]): Promise<void>;
12
+ /** /commit <message> — commit staged changes */
13
+ export declare function handleCommit(ctx: Context, args: string[]): Promise<void>;
14
+ /** /stash [pop|list|drop|save] — stash operations */
15
+ export declare function handleStash(ctx: Context, args: string[]): Promise<void>;
16
+ /** /branch [name] — list branches or create a new one */
17
+ export declare function handleBranch(ctx: Context, args: string[]): Promise<void>;
18
+ /** /checkout <branch> — switch to a branch */
19
+ export declare function handleCheckout(ctx: Context, args: string[]): Promise<void>;
20
+ /** /diff — show git diff summary */
21
+ export declare function handleDiff(ctx: Context): Promise<void>;
22
+ /** /pr <title> — create a pull request */
23
+ export declare function handlePR(ctx: Context, args: string[]): Promise<void>;
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Git command handlers — stage, commit, stash, branch, checkout, diff, PR.
3
+ *
4
+ * Uses direct git subprocess calls for fast, local operations.
5
+ * PR creation uses `gh` CLI (same as the locus CLI does).
6
+ */
7
+ import { execSync } from "node:child_process";
8
+ import { codeBlock, formatError, formatSuccess, bold, escapeHtml } from "../ui/format.js";
9
+ import { stashKeyboard } from "../ui/keyboards.js";
10
+ import { gitBranchCreatedMessage, gitCheckoutMessage, gitCommitMessage, gitStashMessage, prCreatedMessage, } from "../ui/messages.js";
11
+ // ─── Git Helper ─────────────────────────────────────────────────────────────
12
+ function git(args) {
13
+ return execSync(`git ${args}`, {
14
+ encoding: "utf-8",
15
+ stdio: ["pipe", "pipe", "pipe"],
16
+ cwd: process.cwd(),
17
+ });
18
+ }
19
+ function gitSafe(args) {
20
+ try {
21
+ return git(args);
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ // ─── Command Handlers ───────────────────────────────────────────────────────
28
+ /** /gitstatus — show git status */
29
+ export async function handleGitStatus(ctx) {
30
+ try {
31
+ const status = git("status --short");
32
+ if (!status.trim()) {
33
+ await ctx.reply(formatSuccess("Working tree is clean."), {
34
+ parse_mode: "HTML",
35
+ });
36
+ return;
37
+ }
38
+ const branch = git("branch --show-current").trim();
39
+ await ctx.reply(`${bold("Branch:")} ${escapeHtml(branch)}\n\n${codeBlock(status)}`, { parse_mode: "HTML" });
40
+ }
41
+ catch (error) {
42
+ await ctx.reply(formatError("Failed to get git status", String(error)), { parse_mode: "HTML" });
43
+ }
44
+ }
45
+ /** /stage [files|.] — stage files for commit */
46
+ export async function handleStage(ctx, args) {
47
+ const target = args.length > 0 ? args.join(" ") : ".";
48
+ try {
49
+ git(`add ${target}`);
50
+ const status = git("status --short");
51
+ await ctx.reply(`${formatSuccess(`Staged: ${target}`)}\n\n${codeBlock(status)}`, { parse_mode: "HTML" });
52
+ }
53
+ catch (error) {
54
+ await ctx.reply(formatError("Failed to stage files", String(error)), {
55
+ parse_mode: "HTML",
56
+ });
57
+ }
58
+ }
59
+ /** /commit <message> — commit staged changes */
60
+ export async function handleCommit(ctx, args) {
61
+ if (args.length === 0) {
62
+ await ctx.reply(formatError("Usage: /commit <message>"), {
63
+ parse_mode: "HTML",
64
+ });
65
+ return;
66
+ }
67
+ const message = args.join(" ");
68
+ try {
69
+ const result = git(`commit -m ${JSON.stringify(message)}`);
70
+ // Extract short hash from commit output
71
+ const hashMatch = result.match(/\[[\w/.-]+ ([a-f0-9]+)\]/);
72
+ const hash = hashMatch?.[1] ?? "unknown";
73
+ await ctx.reply(gitCommitMessage(message, hash), {
74
+ parse_mode: "HTML",
75
+ });
76
+ }
77
+ catch (error) {
78
+ const errStr = String(error);
79
+ if (errStr.includes("nothing to commit")) {
80
+ await ctx.reply(formatError("Nothing to commit. Stage changes first with /stage"), {
81
+ parse_mode: "HTML",
82
+ });
83
+ }
84
+ else {
85
+ await ctx.reply(formatError("Failed to commit", errStr), {
86
+ parse_mode: "HTML",
87
+ });
88
+ }
89
+ }
90
+ }
91
+ /** /stash [pop|list|drop|save] — stash operations */
92
+ export async function handleStash(ctx, args) {
93
+ const subcommand = args[0] ?? "push";
94
+ try {
95
+ switch (subcommand) {
96
+ case "push":
97
+ case "save": {
98
+ const message = args.slice(1).join(" ");
99
+ const stashArgs = message
100
+ ? `stash push -m ${JSON.stringify(message)}`
101
+ : "stash push";
102
+ git(stashArgs);
103
+ await ctx.reply(gitStashMessage("Changes stashed"), {
104
+ parse_mode: "HTML",
105
+ reply_markup: stashKeyboard(),
106
+ });
107
+ break;
108
+ }
109
+ case "pop": {
110
+ git("stash pop");
111
+ await ctx.reply(gitStashMessage("Stash popped"), {
112
+ parse_mode: "HTML",
113
+ });
114
+ break;
115
+ }
116
+ case "list": {
117
+ const list = gitSafe("stash list") ?? "";
118
+ if (!list.trim()) {
119
+ await ctx.reply(formatSuccess("No stashes."), {
120
+ parse_mode: "HTML",
121
+ });
122
+ }
123
+ else {
124
+ await ctx.reply(codeBlock(list), { parse_mode: "HTML" });
125
+ }
126
+ break;
127
+ }
128
+ case "drop": {
129
+ const stashRef = args[1] ?? "stash@{0}";
130
+ git(`stash drop ${stashRef}`);
131
+ await ctx.reply(gitStashMessage(`Dropped ${stashRef}`), {
132
+ parse_mode: "HTML",
133
+ });
134
+ break;
135
+ }
136
+ default: {
137
+ // Default: just stash
138
+ git("stash push");
139
+ await ctx.reply(gitStashMessage("Changes stashed"), {
140
+ parse_mode: "HTML",
141
+ reply_markup: stashKeyboard(),
142
+ });
143
+ }
144
+ }
145
+ }
146
+ catch (error) {
147
+ await ctx.reply(formatError("Stash operation failed", String(error)), {
148
+ parse_mode: "HTML",
149
+ });
150
+ }
151
+ }
152
+ /** /branch [name] — list branches or create a new one */
153
+ export async function handleBranch(ctx, args) {
154
+ try {
155
+ if (args.length === 0) {
156
+ // List branches
157
+ const branches = git("branch -a --format='%(refname:short)'");
158
+ const current = git("branch --show-current").trim();
159
+ await ctx.reply(`${bold("Current:")} ${escapeHtml(current)}\n\n${codeBlock(branches)}`, { parse_mode: "HTML" });
160
+ }
161
+ else {
162
+ // Create new branch
163
+ const branchName = args[0];
164
+ git(`branch ${branchName}`);
165
+ await ctx.reply(gitBranchCreatedMessage(branchName), {
166
+ parse_mode: "HTML",
167
+ });
168
+ }
169
+ }
170
+ catch (error) {
171
+ await ctx.reply(formatError("Branch operation failed", String(error)), {
172
+ parse_mode: "HTML",
173
+ });
174
+ }
175
+ }
176
+ /** /checkout <branch> — switch to a branch */
177
+ export async function handleCheckout(ctx, args) {
178
+ if (args.length === 0) {
179
+ await ctx.reply(formatError("Usage: /checkout <branch>"), {
180
+ parse_mode: "HTML",
181
+ });
182
+ return;
183
+ }
184
+ const branch = args[0];
185
+ try {
186
+ git(`checkout ${branch}`);
187
+ await ctx.reply(gitCheckoutMessage(branch), { parse_mode: "HTML" });
188
+ }
189
+ catch (error) {
190
+ await ctx.reply(formatError("Checkout failed", String(error)), {
191
+ parse_mode: "HTML",
192
+ });
193
+ }
194
+ }
195
+ /** /diff — show git diff summary */
196
+ export async function handleDiff(ctx) {
197
+ try {
198
+ const diff = git("diff --stat");
199
+ if (!diff.trim()) {
200
+ const staged = git("diff --cached --stat");
201
+ if (!staged.trim()) {
202
+ await ctx.reply(formatSuccess("No changes."), {
203
+ parse_mode: "HTML",
204
+ });
205
+ }
206
+ else {
207
+ await ctx.reply(`${bold("Staged changes:")}\n\n${codeBlock(staged)}`, { parse_mode: "HTML" });
208
+ }
209
+ }
210
+ else {
211
+ await ctx.reply(`${bold("Unstaged changes:")}\n\n${codeBlock(diff)}`, { parse_mode: "HTML" });
212
+ }
213
+ }
214
+ catch (error) {
215
+ await ctx.reply(formatError("Diff failed", String(error)), {
216
+ parse_mode: "HTML",
217
+ });
218
+ }
219
+ }
220
+ /** /pr <title> — create a pull request */
221
+ export async function handlePR(ctx, args) {
222
+ if (args.length === 0) {
223
+ await ctx.reply(formatError("Usage: /pr <title>"), {
224
+ parse_mode: "HTML",
225
+ });
226
+ return;
227
+ }
228
+ const title = args.join(" ");
229
+ try {
230
+ // Push current branch first
231
+ const branch = git("branch --show-current").trim();
232
+ try {
233
+ git(`push -u origin ${branch}`);
234
+ }
235
+ catch {
236
+ // May already be pushed — continue
237
+ }
238
+ // Create PR using gh CLI
239
+ const result = execSync(`gh pr create --title ${JSON.stringify(title)} --body "Created via Locus Telegram Bot" --head ${branch}`, {
240
+ encoding: "utf-8",
241
+ stdio: ["pipe", "pipe", "pipe"],
242
+ cwd: process.cwd(),
243
+ });
244
+ const prMatch = result.match(/\/pull\/(\d+)/);
245
+ const prNumber = prMatch ? Number(prMatch[1]) : 0;
246
+ await ctx.reply(prCreatedMessage(prNumber, result.trim()), {
247
+ parse_mode: "HTML",
248
+ });
249
+ }
250
+ catch (error) {
251
+ await ctx.reply(formatError("Failed to create PR", String(error)), {
252
+ parse_mode: "HTML",
253
+ });
254
+ }
255
+ }
@@ -0,0 +1,12 @@
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 type { Context } from "grammy";
9
+ /**
10
+ * Execute a locus CLI command, streaming output to the Telegram chat.
11
+ */
12
+ export declare function handleLocusCommand(ctx: Context, command: string, args: string[]): Promise<void>;