@locusai/locus-telegram 0.21.9 → 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.
package/README.md CHANGED
@@ -28,7 +28,7 @@ Multiple chat IDs can be comma-separated: `"12345678,87654321"`
28
28
  ### 4. Install & Start
29
29
 
30
30
  ```sh
31
- locus install locus-telegram
31
+ locus install telegram
32
32
  locus pkg telegram start
33
33
  ```
34
34
 
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "@locusai/locus-telegram",
3
- "version": "0.21.9",
3
+ "version": "0.21.11",
4
4
  "description": "Remote-control Locus via Telegram with full CLI mapping, git operations, and PM2 management",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "locus-telegram": "./bin/locus-telegram.js"
8
8
  },
9
+ "files": [
10
+ "bin",
11
+ "dist",
12
+ "package.json",
13
+ "README.md"
14
+ ],
9
15
  "locus": {
10
16
  "displayName": "Telegram",
11
17
  "description": "Remote-control your Locus agent from Telegram",
@@ -21,7 +27,7 @@
21
27
  "format": "biome format --write ."
22
28
  },
23
29
  "dependencies": {
24
- "@locusai/sdk": "^0.21.9",
30
+ "@locusai/sdk": "^0.21.11",
25
31
  "grammy": "^1.35.0",
26
32
  "pm2": "^6.0.5"
27
33
  },
package/CHANGELOG.md DELETED
@@ -1,26 +0,0 @@
1
- # @locusai/locus-telegram
2
-
3
- ## 0.21.9
4
-
5
- ### Patch Changes
6
-
7
- - Locus package names convention fixes
8
- - Updated dependencies
9
- - @locusai/sdk@0.21.9
10
-
11
- ## 0.21.8
12
-
13
- ### Patch Changes
14
-
15
- - Continious learning improvements
16
- Fix Telegram package installation issue
17
- - Updated dependencies
18
- - @locusai/sdk@0.21.8
19
-
20
- ## 0.21.7
21
-
22
- ### Patch Changes
23
-
24
- - Initial telegram package
25
- - Updated dependencies
26
- - @locusai/sdk@0.21.7
package/src/bot.ts DELETED
@@ -1,256 +0,0 @@
1
- /**
2
- * Bot instance — sets up grammy bot, registers middleware, commands,
3
- * and callback query handlers.
4
- */
5
-
6
- import { createLogger } from "@locusai/sdk";
7
- import { Bot } from "grammy";
8
- import {
9
- handleBranch,
10
- handleCheckout,
11
- handleCommit,
12
- handleDiff,
13
- handleGitStatus,
14
- handlePR,
15
- handleStage,
16
- handleStash,
17
- } from "./commands/git.js";
18
- import { handleLocusCommand } from "./commands/locus.js";
19
- import { handleService } from "./commands/service.js";
20
- import type { TelegramConfig } from "./config.js";
21
- import { formatInfo, formatSuccess } from "./ui/format.js";
22
- import { CB } from "./ui/keyboards.js";
23
- import { welcomeMessage } from "./ui/messages.js";
24
-
25
- const logger = createLogger("telegram");
26
-
27
- // ─── Bot Factory ────────────────────────────────────────────────────────────
28
-
29
- export function createBot(config: TelegramConfig): Bot {
30
- const bot = new Bot(config.botToken);
31
-
32
- // ── Auth Middleware ─────────────────────────────────────────────────────
33
- bot.use(async (ctx, next) => {
34
- const chatId = ctx.chat?.id;
35
- if (!chatId || !config.allowedChatIds.includes(chatId)) {
36
- logger.warn("Unauthorized access attempt", {
37
- chatId,
38
- from: ctx.from?.username,
39
- });
40
- return; // silently ignore
41
- }
42
- await next();
43
- });
44
-
45
- // ── Help / Start ────────────────────────────────────────────────────────
46
- bot.command("start", async (ctx) => {
47
- await ctx.reply(welcomeMessage(), { parse_mode: "HTML" });
48
- });
49
-
50
- bot.command("help", async (ctx) => {
51
- await ctx.reply(welcomeMessage(), { parse_mode: "HTML" });
52
- });
53
-
54
- // ── Locus CLI Commands ──────────────────────────────────────────────────
55
- const locusCommands = [
56
- "run",
57
- "status",
58
- "issues",
59
- "issue",
60
- "sprint",
61
- "plan",
62
- "review",
63
- "iterate",
64
- "discuss",
65
- "exec",
66
- "logs",
67
- "config",
68
- "artifacts",
69
- ];
70
-
71
- for (const cmd of locusCommands) {
72
- bot.command(cmd, async (ctx) => {
73
- const args = parseArgs(ctx.message?.text ?? "", cmd);
74
- await handleLocusCommand(ctx, cmd, args);
75
- });
76
- }
77
-
78
- // ── Git Commands ────────────────────────────────────────────────────────
79
- bot.command("gitstatus", async (ctx) => {
80
- await handleGitStatus(ctx);
81
- });
82
-
83
- bot.command("stage", async (ctx) => {
84
- const args = parseArgs(ctx.message?.text ?? "", "stage");
85
- await handleStage(ctx, args);
86
- });
87
-
88
- bot.command("commit", async (ctx) => {
89
- const args = parseArgs(ctx.message?.text ?? "", "commit");
90
- await handleCommit(ctx, args);
91
- });
92
-
93
- bot.command("stash", async (ctx) => {
94
- const args = parseArgs(ctx.message?.text ?? "", "stash");
95
- await handleStash(ctx, args);
96
- });
97
-
98
- bot.command("branch", async (ctx) => {
99
- const args = parseArgs(ctx.message?.text ?? "", "branch");
100
- await handleBranch(ctx, args);
101
- });
102
-
103
- bot.command("checkout", async (ctx) => {
104
- const args = parseArgs(ctx.message?.text ?? "", "checkout");
105
- await handleCheckout(ctx, args);
106
- });
107
-
108
- bot.command("diff", async (ctx) => {
109
- await handleDiff(ctx);
110
- });
111
-
112
- bot.command("pr", async (ctx) => {
113
- const args = parseArgs(ctx.message?.text ?? "", "pr");
114
- await handlePR(ctx, args);
115
- });
116
-
117
- // ── Service Commands ────────────────────────────────────────────────────
118
- bot.command("service", async (ctx) => {
119
- const args = parseArgs(ctx.message?.text ?? "", "service");
120
- await handleService(ctx, args);
121
- });
122
-
123
- // ── Callback Query Handlers ─────────────────────────────────────────────
124
- registerCallbackHandlers(bot);
125
-
126
- // ── Fallback ────────────────────────────────────────────────────────────
127
- bot.on("message:text", async (ctx) => {
128
- // Treat non-command text as a locus exec prompt
129
- const text = ctx.message.text;
130
- if (text.startsWith("/")) return; // unknown command — ignore
131
-
132
- await handleLocusCommand(ctx, "exec", [text]);
133
- });
134
-
135
- return bot;
136
- }
137
-
138
- // ─── Callback Query Handlers ────────────────────────────────────────────────
139
-
140
- function registerCallbackHandlers(bot: Bot): void {
141
- // Plan callbacks
142
- bot.callbackQuery(CB.APPROVE_PLAN, async (ctx) => {
143
- await ctx.answerCallbackQuery({ text: "Plan approved!" });
144
- await ctx.editMessageReplyMarkup({ reply_markup: undefined });
145
- await ctx.reply(formatSuccess("Plan approved. Starting execution..."), {
146
- parse_mode: "HTML",
147
- });
148
- // Trigger the run
149
- await handleLocusCommand(ctx, "run", []);
150
- });
151
-
152
- bot.callbackQuery(CB.REJECT_PLAN, async (ctx) => {
153
- await ctx.answerCallbackQuery({ text: "Plan rejected." });
154
- await ctx.editMessageReplyMarkup({ reply_markup: undefined });
155
- await ctx.reply(formatInfo("Plan rejected. No changes will be made."), {
156
- parse_mode: "HTML",
157
- });
158
- });
159
-
160
- bot.callbackQuery(CB.SHOW_PLAN_DETAILS, async (ctx) => {
161
- await ctx.answerCallbackQuery();
162
- await handleLocusCommand(ctx, "plan", ["--show"]);
163
- });
164
-
165
- // Run callbacks
166
- bot.callbackQuery(CB.VIEW_LOGS, async (ctx) => {
167
- await ctx.answerCallbackQuery();
168
- await handleLocusCommand(ctx, "logs", ["--lines", "30"]);
169
- });
170
-
171
- bot.callbackQuery(/^run:again:(\d+)$/, async (ctx) => {
172
- const issue = ctx.match[1];
173
- await ctx.answerCallbackQuery({ text: `Re-running issue #${issue}` });
174
- await ctx.editMessageReplyMarkup({ reply_markup: undefined });
175
- await handleLocusCommand(ctx, "run", [issue]);
176
- });
177
-
178
- // Review callbacks
179
- bot.callbackQuery(/^review:approve:(\d+)$/, async (ctx) => {
180
- const pr = ctx.match[1];
181
- await ctx.answerCallbackQuery({ text: "Approving PR..." });
182
- await ctx.editMessageReplyMarkup({ reply_markup: undefined });
183
- await ctx.reply(formatSuccess(`Approved PR #${pr}`), {
184
- parse_mode: "HTML",
185
- });
186
- });
187
-
188
- bot.callbackQuery(/^review:changes:(\d+)$/, async (ctx) => {
189
- const pr = ctx.match[1];
190
- await ctx.answerCallbackQuery();
191
- await ctx.editMessageReplyMarkup({ reply_markup: undefined });
192
- await ctx.reply(
193
- formatInfo(
194
- `Requesting changes on PR #${pr}. Reply with your feedback and I'll pass it along via /iterate ${pr}`
195
- ),
196
- { parse_mode: "HTML" }
197
- );
198
- });
199
-
200
- bot.callbackQuery(/^review:diff:(\d+)$/, async (ctx) => {
201
- const pr = ctx.match[1];
202
- await ctx.answerCallbackQuery();
203
- await handleLocusCommand(ctx, "review", [pr, "--diff"]);
204
- });
205
-
206
- // Status callbacks
207
- bot.callbackQuery(CB.RUN_SPRINT, async (ctx) => {
208
- await ctx.answerCallbackQuery({ text: "Starting sprint run..." });
209
- await ctx.editMessageReplyMarkup({ reply_markup: undefined });
210
- await handleLocusCommand(ctx, "run", []);
211
- });
212
-
213
- bot.callbackQuery(CB.VIEW_ISSUES, async (ctx) => {
214
- await ctx.answerCallbackQuery();
215
- await handleLocusCommand(ctx, "issues", []);
216
- });
217
-
218
- // Stash callbacks
219
- bot.callbackQuery(CB.STASH_POP, async (ctx) => {
220
- await ctx.answerCallbackQuery();
221
- await handleStash(ctx, ["pop"]);
222
- });
223
-
224
- bot.callbackQuery(CB.STASH_LIST, async (ctx) => {
225
- await ctx.answerCallbackQuery();
226
- await handleStash(ctx, ["list"]);
227
- });
228
-
229
- bot.callbackQuery(CB.STASH_DROP, async (ctx) => {
230
- await ctx.answerCallbackQuery();
231
- await handleStash(ctx, ["drop"]);
232
- });
233
-
234
- // Confirmation callbacks
235
- bot.callbackQuery(CB.CONFIRM_ACTION, async (ctx) => {
236
- await ctx.answerCallbackQuery({ text: "Confirmed!" });
237
- await ctx.editMessageReplyMarkup({ reply_markup: undefined });
238
- });
239
-
240
- bot.callbackQuery(CB.CANCEL_ACTION, async (ctx) => {
241
- await ctx.answerCallbackQuery({ text: "Cancelled." });
242
- await ctx.editMessageReplyMarkup({ reply_markup: undefined });
243
- await ctx.reply(formatInfo("Action cancelled."), { parse_mode: "HTML" });
244
- });
245
- }
246
-
247
- // ─── Helpers ────────────────────────────────────────────────────────────────
248
-
249
- /** Parse arguments from a Telegram command message. */
250
- function parseArgs(text: string, command: string): string[] {
251
- // Remove the /command part (handles @botname suffix too)
252
- const prefixRegex = new RegExp(`^/${command}(@\\S+)?\\s*`);
253
- const rest = text.replace(prefixRegex, "").trim();
254
- if (!rest) return [];
255
- return rest.split(/\s+/);
256
- }
@@ -1,309 +0,0 @@
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
-
8
- import { execSync } from "node:child_process";
9
- import type { Context } from "grammy";
10
- import {
11
- bold,
12
- codeBlock,
13
- escapeHtml,
14
- formatError,
15
- formatSuccess,
16
- } from "../ui/format.js";
17
- import { stashKeyboard } from "../ui/keyboards.js";
18
- import {
19
- gitBranchCreatedMessage,
20
- gitCheckoutMessage,
21
- gitCommitMessage,
22
- gitStashMessage,
23
- prCreatedMessage,
24
- } from "../ui/messages.js";
25
-
26
- // ─── Git Helper ─────────────────────────────────────────────────────────────
27
-
28
- function git(args: string): string {
29
- return execSync(`git ${args}`, {
30
- encoding: "utf-8",
31
- stdio: ["pipe", "pipe", "pipe"],
32
- cwd: process.cwd(),
33
- });
34
- }
35
-
36
- function gitSafe(args: string): string | null {
37
- try {
38
- return git(args);
39
- } catch {
40
- return null;
41
- }
42
- }
43
-
44
- // ─── Command Handlers ───────────────────────────────────────────────────────
45
-
46
- /** /gitstatus — show git status */
47
- export async function handleGitStatus(ctx: Context): Promise<void> {
48
- try {
49
- const status = git("status --short");
50
- if (!status.trim()) {
51
- await ctx.reply(formatSuccess("Working tree is clean."), {
52
- parse_mode: "HTML",
53
- });
54
- return;
55
- }
56
-
57
- const branch = git("branch --show-current").trim();
58
- await ctx.reply(
59
- `${bold("Branch:")} ${escapeHtml(branch)}\n\n${codeBlock(status)}`,
60
- { parse_mode: "HTML" }
61
- );
62
- } catch (error: unknown) {
63
- await ctx.reply(formatError("Failed to get git status", String(error)), {
64
- parse_mode: "HTML",
65
- });
66
- }
67
- }
68
-
69
- /** /stage [files|.] — stage files for commit */
70
- export async function handleStage(ctx: Context, args: string[]): Promise<void> {
71
- const target = args.length > 0 ? args.join(" ") : ".";
72
-
73
- try {
74
- git(`add ${target}`);
75
- const status = git("status --short");
76
- await ctx.reply(
77
- `${formatSuccess(`Staged: ${target}`)}\n\n${codeBlock(status)}`,
78
- { parse_mode: "HTML" }
79
- );
80
- } catch (error: unknown) {
81
- await ctx.reply(formatError("Failed to stage files", String(error)), {
82
- parse_mode: "HTML",
83
- });
84
- }
85
- }
86
-
87
- /** /commit <message> — commit staged changes */
88
- export async function handleCommit(
89
- ctx: Context,
90
- args: string[]
91
- ): Promise<void> {
92
- if (args.length === 0) {
93
- await ctx.reply(formatError("Usage: /commit <message>"), {
94
- parse_mode: "HTML",
95
- });
96
- return;
97
- }
98
-
99
- const message = args.join(" ");
100
-
101
- try {
102
- const result = git(`commit -m ${JSON.stringify(message)}`);
103
- // Extract short hash from commit output
104
- const hashMatch = result.match(/\[[\w/.-]+ ([a-f0-9]+)\]/);
105
- const hash = hashMatch?.[1] ?? "unknown";
106
-
107
- await ctx.reply(gitCommitMessage(message, hash), {
108
- parse_mode: "HTML",
109
- });
110
- } catch (error: unknown) {
111
- const errStr = String(error);
112
- if (errStr.includes("nothing to commit")) {
113
- await ctx.reply(
114
- formatError("Nothing to commit. Stage changes first with /stage"),
115
- {
116
- parse_mode: "HTML",
117
- }
118
- );
119
- } else {
120
- await ctx.reply(formatError("Failed to commit", errStr), {
121
- parse_mode: "HTML",
122
- });
123
- }
124
- }
125
- }
126
-
127
- /** /stash [pop|list|drop|save] — stash operations */
128
- export async function handleStash(ctx: Context, args: string[]): Promise<void> {
129
- const subcommand = args[0] ?? "push";
130
-
131
- try {
132
- switch (subcommand) {
133
- case "push":
134
- case "save": {
135
- const message = args.slice(1).join(" ");
136
- const stashArgs = message
137
- ? `stash push -m ${JSON.stringify(message)}`
138
- : "stash push";
139
- git(stashArgs);
140
- await ctx.reply(gitStashMessage("Changes stashed"), {
141
- parse_mode: "HTML",
142
- reply_markup: stashKeyboard(),
143
- });
144
- break;
145
- }
146
- case "pop": {
147
- git("stash pop");
148
- await ctx.reply(gitStashMessage("Stash popped"), {
149
- parse_mode: "HTML",
150
- });
151
- break;
152
- }
153
- case "list": {
154
- const list = gitSafe("stash list") ?? "";
155
- if (!list.trim()) {
156
- await ctx.reply(formatSuccess("No stashes."), {
157
- parse_mode: "HTML",
158
- });
159
- } else {
160
- await ctx.reply(codeBlock(list), { parse_mode: "HTML" });
161
- }
162
- break;
163
- }
164
- case "drop": {
165
- const stashRef = args[1] ?? "stash@{0}";
166
- git(`stash drop ${stashRef}`);
167
- await ctx.reply(gitStashMessage(`Dropped ${stashRef}`), {
168
- parse_mode: "HTML",
169
- });
170
- break;
171
- }
172
- default: {
173
- // Default: just stash
174
- git("stash push");
175
- await ctx.reply(gitStashMessage("Changes stashed"), {
176
- parse_mode: "HTML",
177
- reply_markup: stashKeyboard(),
178
- });
179
- }
180
- }
181
- } catch (error: unknown) {
182
- await ctx.reply(formatError("Stash operation failed", String(error)), {
183
- parse_mode: "HTML",
184
- });
185
- }
186
- }
187
-
188
- /** /branch [name] — list branches or create a new one */
189
- export async function handleBranch(
190
- ctx: Context,
191
- args: string[]
192
- ): Promise<void> {
193
- try {
194
- if (args.length === 0) {
195
- // List branches
196
- const branches = git("branch -a --format='%(refname:short)'");
197
- const current = git("branch --show-current").trim();
198
- await ctx.reply(
199
- `${bold("Current:")} ${escapeHtml(current)}\n\n${codeBlock(branches)}`,
200
- { parse_mode: "HTML" }
201
- );
202
- } else {
203
- // Create new branch
204
- const branchName = args[0];
205
- git(`branch ${branchName}`);
206
- await ctx.reply(gitBranchCreatedMessage(branchName), {
207
- parse_mode: "HTML",
208
- });
209
- }
210
- } catch (error: unknown) {
211
- await ctx.reply(formatError("Branch operation failed", String(error)), {
212
- parse_mode: "HTML",
213
- });
214
- }
215
- }
216
-
217
- /** /checkout <branch> — switch to a branch */
218
- export async function handleCheckout(
219
- ctx: Context,
220
- args: string[]
221
- ): Promise<void> {
222
- if (args.length === 0) {
223
- await ctx.reply(formatError("Usage: /checkout <branch>"), {
224
- parse_mode: "HTML",
225
- });
226
- return;
227
- }
228
-
229
- const branch = args[0];
230
-
231
- try {
232
- git(`checkout ${branch}`);
233
- await ctx.reply(gitCheckoutMessage(branch), { parse_mode: "HTML" });
234
- } catch (error: unknown) {
235
- await ctx.reply(formatError("Checkout failed", String(error)), {
236
- parse_mode: "HTML",
237
- });
238
- }
239
- }
240
-
241
- /** /diff — show git diff summary */
242
- export async function handleDiff(ctx: Context): Promise<void> {
243
- try {
244
- const diff = git("diff --stat");
245
- if (!diff.trim()) {
246
- const staged = git("diff --cached --stat");
247
- if (!staged.trim()) {
248
- await ctx.reply(formatSuccess("No changes."), {
249
- parse_mode: "HTML",
250
- });
251
- } else {
252
- await ctx.reply(`${bold("Staged changes:")}\n\n${codeBlock(staged)}`, {
253
- parse_mode: "HTML",
254
- });
255
- }
256
- } else {
257
- await ctx.reply(`${bold("Unstaged changes:")}\n\n${codeBlock(diff)}`, {
258
- parse_mode: "HTML",
259
- });
260
- }
261
- } catch (error: unknown) {
262
- await ctx.reply(formatError("Diff failed", String(error)), {
263
- parse_mode: "HTML",
264
- });
265
- }
266
- }
267
-
268
- /** /pr <title> — create a pull request */
269
- export async function handlePR(ctx: Context, args: string[]): Promise<void> {
270
- if (args.length === 0) {
271
- await ctx.reply(formatError("Usage: /pr <title>"), {
272
- parse_mode: "HTML",
273
- });
274
- return;
275
- }
276
-
277
- const title = args.join(" ");
278
-
279
- try {
280
- // Push current branch first
281
- const branch = git("branch --show-current").trim();
282
- try {
283
- git(`push -u origin ${branch}`);
284
- } catch {
285
- // May already be pushed — continue
286
- }
287
-
288
- // Create PR using gh CLI
289
- const result = execSync(
290
- `gh pr create --title ${JSON.stringify(title)} --body "Created via Locus Telegram Bot" --head ${branch}`,
291
- {
292
- encoding: "utf-8",
293
- stdio: ["pipe", "pipe", "pipe"],
294
- cwd: process.cwd(),
295
- }
296
- );
297
-
298
- const prMatch = result.match(/\/pull\/(\d+)/);
299
- const prNumber = prMatch ? Number(prMatch[1]) : 0;
300
-
301
- await ctx.reply(prCreatedMessage(prNumber, result.trim()), {
302
- parse_mode: "HTML",
303
- });
304
- } catch (error: unknown) {
305
- await ctx.reply(formatError("Failed to create PR", String(error)), {
306
- parse_mode: "HTML",
307
- });
308
- }
309
- }