kiro-telegram-bot 1.5.1

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.
Files changed (83) hide show
  1. package/.env.example +104 -0
  2. package/LICENSE +21 -0
  3. package/README.md +517 -0
  4. package/bin/kiro-tg.mjs +21 -0
  5. package/docs/INSTALL.md +143 -0
  6. package/docs/ops/RELEASE_CHECKLIST.md +39 -0
  7. package/package.json +70 -0
  8. package/scripts/mq.ts +25 -0
  9. package/scripts/setup.mjs +78 -0
  10. package/src/acp/client.ts +456 -0
  11. package/src/acp/server-handlers.ts +85 -0
  12. package/src/acp/transport.ts +50 -0
  13. package/src/acp/types.ts +136 -0
  14. package/src/agents/catalog.ts +44 -0
  15. package/src/app/json-store.ts +54 -0
  16. package/src/app/reasoning.ts +30 -0
  17. package/src/app/settings-store.ts +31 -0
  18. package/src/app/stt.ts +53 -0
  19. package/src/app/types.ts +48 -0
  20. package/src/app/usage.ts +32 -0
  21. package/src/bot/auth.ts +27 -0
  22. package/src/bot/bot.ts +154 -0
  23. package/src/bot/chat-controller.ts +251 -0
  24. package/src/bot/commands.ts +48 -0
  25. package/src/bot/deps.ts +47 -0
  26. package/src/bot/handlers/control.ts +94 -0
  27. package/src/bot/handlers/history.ts +58 -0
  28. package/src/bot/handlers/kill.ts +69 -0
  29. package/src/bot/handlers/mcp.ts +205 -0
  30. package/src/bot/handlers/menu.ts +204 -0
  31. package/src/bot/handlers/message.ts +93 -0
  32. package/src/bot/handlers/photo.ts +108 -0
  33. package/src/bot/handlers/projects.ts +83 -0
  34. package/src/bot/handlers/running.ts +104 -0
  35. package/src/bot/handlers/session-card.ts +65 -0
  36. package/src/bot/handlers/sessions.ts +131 -0
  37. package/src/bot/handlers/system.ts +51 -0
  38. package/src/bot/handlers/tasks.ts +223 -0
  39. package/src/bot/handlers/usage.ts +33 -0
  40. package/src/bot/handlers/voice.ts +53 -0
  41. package/src/bot/image-return.ts +69 -0
  42. package/src/bot/menu/keyboard.ts +47 -0
  43. package/src/bot/menu/refresh.ts +13 -0
  44. package/src/bot/menu/status-panel.ts +78 -0
  45. package/src/bot/permission-service.ts +149 -0
  46. package/src/bot/prompt-content.ts +49 -0
  47. package/src/bot/prompt-retry.ts +70 -0
  48. package/src/bot/registry.ts +178 -0
  49. package/src/bot/session-runtime.ts +670 -0
  50. package/src/bot/telegram-io.ts +109 -0
  51. package/src/bot/typing.ts +35 -0
  52. package/src/bot/wizard/task-wizard.ts +214 -0
  53. package/src/cli.ts +125 -0
  54. package/src/config.ts +190 -0
  55. package/src/index.ts +74 -0
  56. package/src/logger.ts +78 -0
  57. package/src/mcp/config.ts +103 -0
  58. package/src/mcp/probe.ts +218 -0
  59. package/src/mcp/types.ts +68 -0
  60. package/src/projects/manager.ts +88 -0
  61. package/src/render/chunk.ts +57 -0
  62. package/src/render/diff.ts +48 -0
  63. package/src/render/escape.ts +22 -0
  64. package/src/render/markdown.ts +126 -0
  65. package/src/render/subagent.ts +75 -0
  66. package/src/render/tool-call.ts +102 -0
  67. package/src/service/index.ts +24 -0
  68. package/src/service/linux.ts +83 -0
  69. package/src/service/macos.ts +91 -0
  70. package/src/service/platform.ts +59 -0
  71. package/src/service/types.ts +34 -0
  72. package/src/service/windows.ts +103 -0
  73. package/src/sessions/history.ts +181 -0
  74. package/src/sessions/store.ts +133 -0
  75. package/src/sessions/tail.ts +86 -0
  76. package/src/sessions/types.ts +26 -0
  77. package/src/stream/streamer.ts +167 -0
  78. package/src/tasks/runner.ts +82 -0
  79. package/src/tasks/schedule.ts +142 -0
  80. package/src/tasks/scheduler.ts +53 -0
  81. package/src/tasks/store.ts +80 -0
  82. package/src/tasks/types.ts +33 -0
  83. package/tsconfig.json +19 -0
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Executes a scheduled task: opens a fresh session in the task's project,
3
+ * sends the prompt, collects the response, and delivers it to the chat.
4
+ * Runs independently of the user's interactive session.
5
+ */
6
+ import type { Api } from "grammy";
7
+ import { basename } from "node:path";
8
+ import type { AcpClient } from "../acp/client.js";
9
+ import type { SessionUpdate } from "../acp/types.js";
10
+ import { createLogger } from "../logger.js";
11
+ import { sendMarkdownDoc } from "../bot/telegram-io.js";
12
+ import type { Task } from "./types.js";
13
+
14
+ const log = createLogger("task-runner");
15
+
16
+ export class TaskRunner {
17
+ constructor(
18
+ private readonly api: Api,
19
+ private readonly acp: AcpClient,
20
+ ) {}
21
+
22
+ /** Run a task; resolves true on success, false on error. */
23
+ async run(task: Task): Promise<boolean> {
24
+ log.info(`running task "${task.name}" in ${task.projectPath}`);
25
+ let sessionId = "";
26
+ let text = "";
27
+ let tools = 0;
28
+ const seen = new Set<string>();
29
+
30
+ const listener = (sid: string, u: SessionUpdate): void => {
31
+ if (sid !== sessionId) return;
32
+ if (u.sessionUpdate === "agent_message_chunk" && typeof u.content?.text === "string") {
33
+ text += u.content.text;
34
+ } else if (u.sessionUpdate === "tool_call") {
35
+ const id = u.toolCallId || u.title || String(tools);
36
+ if (!seen.has(id)) {
37
+ seen.add(id);
38
+ tools++;
39
+ }
40
+ }
41
+ };
42
+
43
+ try {
44
+ sessionId = await this.acp.newSession(task.projectPath);
45
+ if (task.agent) {
46
+ try {
47
+ await this.acp.setMode(sessionId, task.agent);
48
+ } catch {
49
+ /* best-effort */
50
+ }
51
+ }
52
+ this.acp.on("session-update", listener);
53
+ await this.acp.prompt(sessionId, [{ type: "text", text: task.prompt }]);
54
+ this.acp.off("session-update", listener);
55
+ await this.deliver(task, text, tools);
56
+ return true;
57
+ } catch (err) {
58
+ this.acp.off("session-update", listener);
59
+ await this.deliverError(task, (err as Error).message);
60
+ log.error(`task "${task.name}" failed:`, (err as Error).message);
61
+ return false;
62
+ }
63
+ }
64
+
65
+ private async deliver(task: Task, text: string, tools: number): Promise<void> {
66
+ const project = task.projectName || basename(task.projectPath);
67
+ const body = text.trim() || "_(no text output)_";
68
+ const footer = tools > 0 ? `\n\n\u{1F527} ${tools} tool call(s)` : "";
69
+ const header = `\u23F0 **Task: ${task.name}** \u00B7 ${project}`;
70
+ await sendMarkdownDoc(this.api, task.chatId, `${header}\n\n${body}${footer}`, { loud: true });
71
+ }
72
+
73
+ private async deliverError(task: Task, message: string): Promise<void> {
74
+ try {
75
+ await this.api.sendMessage(task.chatId, `\u274C Task "${task.name}" failed: ${message}`, {
76
+ disable_notification: false,
77
+ });
78
+ } catch {
79
+ /* non-fatal */
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Schedule helpers: parse user input, compute the next run time (local
3
+ * timezone), and produce human-readable descriptions.
4
+ */
5
+ import type { Schedule, ScheduleType } from "./types.js";
6
+
7
+ const DAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
8
+ const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
9
+
10
+ export function parseTime(s: string): { h: number; m: number } | undefined {
11
+ const m = /^(\d{1,2}):(\d{2})$/.exec(s.trim());
12
+ if (!m) return undefined;
13
+ const h = Number(m[1]);
14
+ const min = Number(m[2]);
15
+ if (h > 23 || min > 59) return undefined;
16
+ return { h, m: min };
17
+ }
18
+
19
+ /** Parse the free-text detail for a given schedule type into a Schedule. */
20
+ export function parseScheduleDetail(
21
+ type: ScheduleType,
22
+ text: string,
23
+ ): { schedule?: Schedule; error?: string } {
24
+ const t = text.trim();
25
+ switch (type) {
26
+ case "once": {
27
+ const ms = Date.parse(t.replace(" ", "T"));
28
+ if (Number.isNaN(ms)) return { error: "Use format: YYYY-MM-DD HH:MM" };
29
+ if (ms <= Date.now()) return { error: "That time is in the past." };
30
+ return { schedule: { type, at: new Date(ms).toISOString() } };
31
+ }
32
+ case "daily": {
33
+ const time = parseTime(t);
34
+ if (!time) return { error: "Use format: HH:MM (e.g. 09:30)" };
35
+ return { schedule: { type, time: fmt(time) } };
36
+ }
37
+ case "weekly": {
38
+ const [dayStr, timeStr] = t.split(/\s+/);
39
+ const weekday = DAYS.indexOf((dayStr ?? "").slice(0, 3).toLowerCase());
40
+ const time = parseTime(timeStr ?? "");
41
+ if (weekday < 0 || !time) return { error: "Use format: Mon 09:30" };
42
+ return { schedule: { type, weekday, time: fmt(time) } };
43
+ }
44
+ case "monthly": {
45
+ const [dayStr, timeStr] = t.split(/\s+/);
46
+ const day = Number(dayStr);
47
+ const time = parseTime(timeStr ?? "");
48
+ if (!Number.isInteger(day) || day < 1 || day > 31 || !time) {
49
+ return { error: "Use format: 15 09:30 (day-of-month time)" };
50
+ }
51
+ return { schedule: { type, day, time: fmt(time) } };
52
+ }
53
+ case "interval": {
54
+ const mins = Number(t);
55
+ if (!Number.isInteger(mins) || mins < 1) return { error: "Enter minutes, e.g. 90" };
56
+ return { schedule: { type, everyMinutes: mins } };
57
+ }
58
+ default:
59
+ return { error: "Unknown schedule type." };
60
+ }
61
+ }
62
+
63
+ /** Next run time in epoch ms, or undefined if the schedule will never fire. */
64
+ export function computeNextRun(schedule: Schedule, from = Date.now()): number | undefined {
65
+ switch (schedule.type) {
66
+ case "once": {
67
+ const ms = schedule.at ? Date.parse(schedule.at) : NaN;
68
+ return !Number.isNaN(ms) && ms > from ? ms : undefined;
69
+ }
70
+ case "interval":
71
+ return from + (schedule.everyMinutes ?? 60) * 60_000;
72
+ case "daily":
73
+ return nextDaily(schedule, from);
74
+ case "weekly":
75
+ return nextWeekly(schedule, from);
76
+ case "monthly":
77
+ return nextMonthly(schedule, from);
78
+ default:
79
+ return undefined;
80
+ }
81
+ }
82
+
83
+ function nextDaily(s: Schedule, from: number): number {
84
+ const { h, m } = splitTime(s.time);
85
+ const d = new Date(from);
86
+ d.setHours(h, m, 0, 0);
87
+ if (d.getTime() <= from) d.setDate(d.getDate() + 1);
88
+ return d.getTime();
89
+ }
90
+
91
+ function nextWeekly(s: Schedule, from: number): number {
92
+ const { h, m } = splitTime(s.time);
93
+ const target = s.weekday ?? 1;
94
+ const d = new Date(from);
95
+ d.setHours(h, m, 0, 0);
96
+ for (let i = 0; i < 8; i++) {
97
+ if (d.getDay() === target && d.getTime() > from) return d.getTime();
98
+ d.setDate(d.getDate() + 1);
99
+ d.setHours(h, m, 0, 0);
100
+ }
101
+ return d.getTime();
102
+ }
103
+
104
+ function nextMonthly(s: Schedule, from: number): number {
105
+ const { h, m } = splitTime(s.time);
106
+ const day = s.day ?? 1;
107
+ const base = new Date(from);
108
+ for (let i = 0; i < 13; i++) {
109
+ const year = base.getFullYear();
110
+ const month = base.getMonth() + i;
111
+ const lastDay = new Date(year, month + 1, 0).getDate();
112
+ const d = new Date(year, month, Math.min(day, lastDay), h, m, 0, 0);
113
+ if (d.getTime() > from) return d.getTime();
114
+ }
115
+ return from + 31 * 86_400_000;
116
+ }
117
+
118
+ export function describeSchedule(s: Schedule): string {
119
+ switch (s.type) {
120
+ case "once":
121
+ return `once at ${s.at ? new Date(s.at).toLocaleString() : "?"}`;
122
+ case "daily":
123
+ return `daily at ${s.time}`;
124
+ case "weekly":
125
+ return `weekly on ${DAY_NAMES[s.weekday ?? 1]} at ${s.time}`;
126
+ case "monthly":
127
+ return `monthly on day ${s.day} at ${s.time}`;
128
+ case "interval":
129
+ return `every ${s.everyMinutes} min`;
130
+ default:
131
+ return "unknown";
132
+ }
133
+ }
134
+
135
+ function fmt(t: { h: number; m: number }): string {
136
+ return `${String(t.h).padStart(2, "0")}:${String(t.m).padStart(2, "0")}`;
137
+ }
138
+
139
+ function splitTime(time?: string): { h: number; m: number } {
140
+ const p = parseTime(time ?? "09:00");
141
+ return p ?? { h: 9, m: 0 };
142
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Scheduler — ticks periodically, runs due tasks one at a time, and reschedules
3
+ * them. One-off tasks are disabled after running.
4
+ */
5
+ import { createLogger } from "../logger.js";
6
+ import type { TaskRunner } from "./runner.js";
7
+ import { computeNextRun } from "./schedule.js";
8
+ import type { TaskStore } from "./store.js";
9
+
10
+ const log = createLogger("scheduler");
11
+ const TICK_MS = 30_000;
12
+
13
+ export class Scheduler {
14
+ private timer: NodeJS.Timeout | undefined;
15
+ private running = false;
16
+
17
+ constructor(
18
+ private readonly store: TaskStore,
19
+ private readonly runner: TaskRunner,
20
+ ) {}
21
+
22
+ start(): void {
23
+ if (this.timer) return;
24
+ this.timer = setInterval(() => void this.tick(), TICK_MS);
25
+ log.info(`scheduler started (tick ${TICK_MS / 1000}s)`);
26
+ }
27
+
28
+ stop(): void {
29
+ if (this.timer) clearInterval(this.timer);
30
+ this.timer = undefined;
31
+ }
32
+
33
+ private async tick(): Promise<void> {
34
+ if (this.running) return; // never overlap runs
35
+ const due = this.store.due();
36
+ if (due.length === 0) return;
37
+ this.running = true;
38
+ try {
39
+ for (const task of due) {
40
+ const ok = await this.runner.run(task);
41
+ const isOnce = task.schedule.type === "once";
42
+ this.store.update(task.id, {
43
+ lastRun: Date.now(),
44
+ lastStatus: ok ? "ok" : "error",
45
+ enabled: isOnce ? false : task.enabled,
46
+ nextRun: isOnce ? undefined : computeNextRun(task.schedule, Date.now() + 1000),
47
+ });
48
+ }
49
+ } finally {
50
+ this.running = false;
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Task persistence and CRUD, backed by a JSON file.
3
+ */
4
+ import { randomUUID } from "node:crypto";
5
+ import { join } from "node:path";
6
+ import { JsonStore } from "../app/json-store.js";
7
+ import { computeNextRun } from "./schedule.js";
8
+ import type { Task } from "./types.js";
9
+
10
+ export class TaskStore {
11
+ private readonly store: JsonStore<Task[]>;
12
+
13
+ constructor(dataDir: string) {
14
+ this.store = new JsonStore<Task[]>(join(dataDir, "tasks.json"), []);
15
+ }
16
+
17
+ all(): Task[] {
18
+ return this.store.get();
19
+ }
20
+
21
+ forChat(chatId: number): Task[] {
22
+ return this.all()
23
+ .filter((t) => t.chatId === chatId)
24
+ .sort((a, b) => (a.nextRun ?? Infinity) - (b.nextRun ?? Infinity));
25
+ }
26
+
27
+ get(id: string): Task | undefined {
28
+ return this.all().find((t) => t.id === id);
29
+ }
30
+
31
+ create(input: Omit<Task, "id" | "createdAt" | "enabled" | "nextRun">): Task {
32
+ const task: Task = {
33
+ ...input,
34
+ id: randomUUID(),
35
+ createdAt: Date.now(),
36
+ enabled: true,
37
+ nextRun: computeNextRun(input.schedule),
38
+ };
39
+ this.store.update((list) => {
40
+ list.push(task);
41
+ });
42
+ return task;
43
+ }
44
+
45
+ update(id: string, patch: Partial<Task>): Task | undefined {
46
+ let updated: Task | undefined;
47
+ this.store.update((list) => {
48
+ const i = list.findIndex((t) => t.id === id);
49
+ if (i === -1) return;
50
+ const merged = { ...list[i]!, ...patch };
51
+ // Recompute nextRun automatically unless the caller set it explicitly.
52
+ if (!("nextRun" in patch)) {
53
+ if (!merged.enabled) merged.nextRun = undefined;
54
+ else if (patch.schedule || patch.enabled !== undefined) {
55
+ merged.nextRun = computeNextRun(merged.schedule);
56
+ }
57
+ }
58
+ list[i] = merged;
59
+ updated = merged;
60
+ });
61
+ return updated;
62
+ }
63
+
64
+ delete(id: string): boolean {
65
+ let removed = false;
66
+ this.store.update((list) => {
67
+ const i = list.findIndex((t) => t.id === id);
68
+ if (i !== -1) {
69
+ list.splice(i, 1);
70
+ removed = true;
71
+ }
72
+ });
73
+ return removed;
74
+ }
75
+
76
+ /** Tasks that are enabled and due to run at/under `now`. */
77
+ due(now = Date.now()): Task[] {
78
+ return this.all().filter((t) => t.enabled && t.nextRun !== undefined && t.nextRun <= now);
79
+ }
80
+ }
@@ -0,0 +1,33 @@
1
+ /** Scheduled task model. */
2
+
3
+ export type ScheduleType = "once" | "daily" | "weekly" | "monthly" | "interval";
4
+
5
+ export interface Schedule {
6
+ type: ScheduleType;
7
+ /** ISO datetime for one-off tasks. */
8
+ at?: string;
9
+ /** "HH:MM" 24h local time for daily/weekly/monthly. */
10
+ time?: string;
11
+ /** 0=Sunday … 6=Saturday, for weekly. */
12
+ weekday?: number;
13
+ /** 1..31, for monthly. */
14
+ day?: number;
15
+ /** Minutes between runs, for interval. */
16
+ everyMinutes?: number;
17
+ }
18
+
19
+ export interface Task {
20
+ id: string;
21
+ chatId: number;
22
+ name: string;
23
+ prompt: string;
24
+ projectPath: string;
25
+ projectName?: string;
26
+ agent?: string;
27
+ schedule: Schedule;
28
+ enabled: boolean;
29
+ nextRun?: number; // epoch ms
30
+ lastRun?: number;
31
+ lastStatus?: "ok" | "error" | "running";
32
+ createdAt: number;
33
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2023"],
7
+ "types": ["node"],
8
+ "strict": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "verbatimModuleSyntax": false,
15
+ "noEmit": true
16
+ },
17
+ "include": ["src/**/*.ts"],
18
+ "exclude": ["node_modules"]
19
+ }