maestro-agent 0.0.1 → 0.0.2

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 (94) hide show
  1. package/README.md +316 -2
  2. package/bin/maestro.ts +5 -0
  3. package/dist/maestro +0 -0
  4. package/dist/web/assets/Connections-DV2Kql1Z.js +1 -0
  5. package/dist/web/assets/GanttView-CCT_rFpY.js +39 -0
  6. package/dist/web/assets/Home-BFbUIh2z.js +1 -0
  7. package/dist/web/assets/HooksCrons-ASM5-jDm.js +1 -0
  8. package/dist/web/assets/ProjectDetail-KZZi6IAd.js +1 -0
  9. package/dist/web/assets/Roles-KQ94PG3H.js +4 -0
  10. package/dist/web/assets/ScheduledTasks-CdJHJpEV.js +1 -0
  11. package/dist/web/assets/Settings-CTflMta-.js +1 -0
  12. package/dist/web/assets/Skills-D09W1mwX.js +2 -0
  13. package/dist/web/assets/Wizard-CW6B0wc3.js +1 -0
  14. package/dist/web/assets/WorkspaceChat-CthETL_A.js +1 -0
  15. package/dist/web/assets/WorkspaceDashboard-DTAesQuT.js +1 -0
  16. package/dist/web/assets/WorkspaceNew-Em4msIKn.js +1 -0
  17. package/dist/web/assets/WorkspaceProjects-Dxg2BpQy.js +1 -0
  18. package/dist/web/assets/WorkspaceTasks-C20mnnkP.js +1 -0
  19. package/dist/web/assets/index-B1k33vcR.js +11 -0
  20. package/dist/web/assets/index-Bk2hHz7P.css +1 -0
  21. package/dist/web/assets/index-Ddy5AJwx.js +61 -0
  22. package/dist/web/assets/useEventStream-DTID465I.js +1 -0
  23. package/dist/web/index.html +13 -0
  24. package/package.json +49 -6
  25. package/src/api/agents.ts +76 -0
  26. package/src/api/audit.ts +19 -0
  27. package/src/api/autopilot.ts +73 -0
  28. package/src/api/chat.ts +801 -0
  29. package/src/api/chief.ts +84 -0
  30. package/src/api/config.ts +39 -0
  31. package/src/api/gantt.ts +72 -0
  32. package/src/api/hooks.ts +54 -0
  33. package/src/api/inbox.ts +125 -0
  34. package/src/api/lark.ts +32 -0
  35. package/src/api/memory.ts +37 -0
  36. package/src/api/ops.ts +89 -0
  37. package/src/api/projects.ts +105 -0
  38. package/src/api/roles.ts +123 -0
  39. package/src/api/runtimes.ts +62 -0
  40. package/src/api/scheduled-tasks.ts +203 -0
  41. package/src/api/sessions.ts +479 -0
  42. package/src/api/skills.ts +386 -0
  43. package/src/api/tasks.ts +457 -0
  44. package/src/api/telegram.ts +94 -0
  45. package/src/api/templates.ts +36 -0
  46. package/src/api/webhooks.ts +20 -0
  47. package/src/api/workspaces.ts +150 -0
  48. package/src/bridges/lark/index.ts +213 -0
  49. package/src/bridges/telegram/index.ts +273 -0
  50. package/src/bridges/telegram/polling.ts +185 -0
  51. package/src/chat/index.ts +86 -0
  52. package/src/chief/index.ts +461 -0
  53. package/src/core/cli.ts +333 -0
  54. package/src/core/db.ts +53 -0
  55. package/src/core/event-bus.ts +33 -0
  56. package/src/core/index.ts +6 -0
  57. package/src/core/migrations.ts +303 -0
  58. package/src/core/router.ts +69 -0
  59. package/src/core/schema.sql +232 -0
  60. package/src/core/server.ts +308 -0
  61. package/src/core/validate.ts +22 -0
  62. package/src/discovery/index.ts +194 -0
  63. package/src/gateway/adapters/telegram.ts +148 -0
  64. package/src/gateway/index.ts +31 -0
  65. package/src/gateway/manager.ts +176 -0
  66. package/src/gateway/types.ts +77 -0
  67. package/src/inbox/index.ts +500 -0
  68. package/src/ops/artifact-sync.ts +65 -0
  69. package/src/ops/autopilot.ts +338 -0
  70. package/src/ops/gc.ts +252 -0
  71. package/src/ops/index.ts +226 -0
  72. package/src/ops/project-serial.ts +52 -0
  73. package/src/ops/role-dispatch.ts +111 -0
  74. package/src/ops/runtime-scheduler.ts +447 -0
  75. package/src/ops/task-blocking.ts +65 -0
  76. package/src/ops/task-deps.ts +37 -0
  77. package/src/ops/task-workspace.ts +60 -0
  78. package/src/roles/index.ts +258 -0
  79. package/src/roles/prompt-assembler.ts +85 -0
  80. package/src/roles/workspace-role.ts +155 -0
  81. package/src/scheduler/index.ts +461 -0
  82. package/src/session/output-parser.ts +75 -0
  83. package/src/session/realtime-parser.ts +40 -0
  84. package/src/skills/builtin.ts +155 -0
  85. package/src/skills/skill-extractor.ts +452 -0
  86. package/src/skills/skill-md.ts +282 -0
  87. package/src/transport/http-api.ts +75 -0
  88. package/src/transport/index.ts +4 -0
  89. package/src/transport/local-pty.ts +119 -0
  90. package/src/transport/ssh.ts +176 -0
  91. package/src/transport/types.ts +20 -0
  92. package/src/workflows/index.ts +231 -0
  93. package/index.js +0 -1
  94. package/maestro-agent-0.0.1.tgz +0 -0
@@ -0,0 +1,150 @@
1
+ import { join } from "path";
2
+ import type { Router } from "../core/router";
3
+ import { json, body } from "../core/router";
4
+ import type { HubContext } from "../core/server";
5
+ import { generateId, now } from "../core/db";
6
+ import { required } from "../core/validate";
7
+ import { recommendRoles } from "../chief";
8
+
9
+ export function registerWorkspaceRoutes(router: Router, ctx: HubContext) {
10
+ router.post("/api/workspaces", async (req) => {
11
+ const { name, goal } = await body(req);
12
+ required({ name }, ["name"]);
13
+ const id = generateId("ws");
14
+ const ts = now();
15
+ ctx.db.run(
16
+ "INSERT INTO workspace (id, name, goal, status, created_at, updated_at) VALUES (?, ?, ?, 'active', ?, ?)",
17
+ [id, name, goal || null, ts, ts]
18
+ );
19
+ ctx.bus.publish("workspace.created", { id, name });
20
+ return json({ id, name, goal, status: "active", created_at: ts, updated_at: ts }, 201);
21
+ });
22
+
23
+ router.get("/api/workspaces", () => {
24
+ const rows = ctx.db.query("SELECT * FROM workspace ORDER BY created_at DESC").all();
25
+ return json(rows);
26
+ });
27
+
28
+ router.get("/api/workspaces/:id", (_req, params) => {
29
+ const row = ctx.db.query("SELECT * FROM workspace WHERE id = ?").get(params.id);
30
+ if (!row) return json({ error: "Not found" }, 404);
31
+ return json(row);
32
+ });
33
+
34
+ router.patch("/api/workspaces/:id", async (req, params) => {
35
+ const updates = await body(req);
36
+ const fields: string[] = [];
37
+ const values: any[] = [];
38
+ for (const key of ["name", "goal", "status"]) {
39
+ if (updates[key] !== undefined) {
40
+ fields.push(`${key} = ?`);
41
+ values.push(updates[key]);
42
+ }
43
+ }
44
+ if (fields.length === 0) return json({ error: "No fields to update" }, 400);
45
+ fields.push("updated_at = ?");
46
+ values.push(now());
47
+ values.push(params.id);
48
+ ctx.db.run(`UPDATE workspace SET ${fields.join(", ")} WHERE id = ?`, values);
49
+ const row = ctx.db.query("SELECT * FROM workspace WHERE id = ?").get(params.id);
50
+ return json(row);
51
+ });
52
+
53
+ router.delete("/api/workspaces/:id", (_req, params) => {
54
+ const workspace = ctx.db.query("SELECT * FROM workspace WHERE id = ?").get(params.id) as any;
55
+ if (!workspace) return json({ error: "Not found" }, 404);
56
+ // 检查是否有关联 project
57
+ const projects = ctx.db.query("SELECT id FROM project WHERE workspace_id = ?").all(params.id);
58
+ if (projects.length > 0) return json({ error: "Cannot delete workspace with existing projects" }, 409);
59
+ ctx.db.run("DELETE FROM workspace WHERE id = ?", [params.id]);
60
+ ctx.bus.publish("workspace.deleted", { id: params.id });
61
+ return json({ ok: true });
62
+ });
63
+
64
+ // Recommend roles for a workspace based on its goal
65
+ router.post("/api/workspaces/:id/recommend-roles", (_req, params) => {
66
+ const workspace = ctx.db.query("SELECT * FROM workspace WHERE id = ?").get(params.id) as any;
67
+ if (!workspace) return json({ error: "Workspace not found" }, 404);
68
+ const recommendations = recommendRoles(workspace.goal || workspace.name);
69
+ return json(recommendations);
70
+ });
71
+
72
+ router.post("/api/workspaces/:id/pause", (_req, params) => {
73
+ const workspace = ctx.db.query("SELECT * FROM workspace WHERE id = ?").get(params.id) as any;
74
+ if (!workspace) return json({ error: "Not found" }, 404);
75
+ if (workspace.status !== "active") return json({ error: "Only active workspaces can be paused" }, 409);
76
+ const ts = now();
77
+ ctx.db.run("UPDATE workspace SET status = 'paused', updated_at = ? WHERE id = ?", [ts, params.id]);
78
+ ctx.bus.publish("workspace.paused", { id: params.id });
79
+ const row = ctx.db.query("SELECT * FROM workspace WHERE id = ?").get(params.id);
80
+ return json(row);
81
+ });
82
+
83
+ router.post("/api/workspaces/:id/resume", (_req, params) => {
84
+ const workspace = ctx.db.query("SELECT * FROM workspace WHERE id = ?").get(params.id) as any;
85
+ if (!workspace) return json({ error: "Not found" }, 404);
86
+ if (workspace.status !== "paused") return json({ error: "Only paused workspaces can be resumed" }, 409);
87
+ const ts = now();
88
+ ctx.db.run("UPDATE workspace SET status = 'active', updated_at = ? WHERE id = ?", [ts, params.id]);
89
+ ctx.bus.publish("workspace.resumed", { id: params.id });
90
+ const row = ctx.db.query("SELECT * FROM workspace WHERE id = ?").get(params.id);
91
+ return json(row);
92
+ });
93
+
94
+ // Wizard: create project + assign roles + instantiate agents in one step
95
+ router.post("/api/workspaces/:id/wizard", async (req, params) => {
96
+ const workspace = ctx.db.query("SELECT * FROM workspace WHERE id = ?").get(params.id) as any;
97
+ if (!workspace) return json({ error: "Workspace not found" }, 404);
98
+
99
+ const { project_name, roles } = await body(req);
100
+ if (!project_name) return json({ error: "project_name is required" }, 400);
101
+ if (!roles || !Array.isArray(roles) || roles.length === 0) {
102
+ return json({ error: "roles array is required" }, 400);
103
+ }
104
+
105
+ const ts = now();
106
+
107
+ // 1. Create project
108
+ const projectId = generateId("proj");
109
+ ctx.db.run(
110
+ "INSERT INTO project (id, workspace_id, name, status, created_by, created_at, updated_at) VALUES (?, ?, ?, 'active', 'wizard', ?, ?)",
111
+ [projectId, params.id, project_name, ts, ts]
112
+ );
113
+
114
+ // 2. Assign roles to workspace and instantiate agents
115
+ const createdAgents: any[] = [];
116
+ for (const roleSpec of roles) {
117
+ const { role_id, runtime_id, headcount } = roleSpec;
118
+ if (!role_id || !runtime_id) continue;
119
+
120
+ // Update role's workspace_id and project_id
121
+ ctx.db.run(
122
+ "UPDATE role SET workspace_id = ?, project_id = ?, updated_at = ? WHERE id = ?",
123
+ [params.id, projectId, ts, role_id]
124
+ );
125
+
126
+ const role = ctx.db.query("SELECT * FROM role WHERE id = ?").get(role_id) as any;
127
+ const count = headcount || role?.headcount || 1;
128
+
129
+ // Create agents
130
+ for (let i = 0; i < count; i++) {
131
+ const agentId = generateId("agent");
132
+ const name = `${role?.name || role_id}-${i + 1}`;
133
+ const workdir = null;
134
+ ctx.db.run(
135
+ "INSERT INTO agent (id, role_id, runtime_id, name, workdir, status, created_at, last_active_at) VALUES (?, ?, ?, ?, ?, 'idle', ?, ?)",
136
+ [agentId, role_id, runtime_id, name, workdir, ts, ts]
137
+ );
138
+ createdAgents.push({ id: agentId, name, role_id, runtime_id, status: "idle" });
139
+ ctx.bus.publish("agent.spawned", { id: agentId, name, runtime_id });
140
+ }
141
+ }
142
+
143
+ ctx.bus.publish("project.created", { id: projectId, workspace_id: params.id });
144
+
145
+ return json({
146
+ project: { id: projectId, name: project_name, workspace_id: params.id },
147
+ agents: createdAgents,
148
+ }, 201);
149
+ });
150
+ }
@@ -0,0 +1,213 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { generateId, now } from "../../core/db";
3
+ import { countTasksByPredicate, isActiveTaskStatus, isInitialTaskStatus } from "../../workflows";
4
+
5
+ export interface LarkCard {
6
+ type: "card";
7
+ title: string;
8
+ fields?: Record<string, number>;
9
+ items?: any[];
10
+ }
11
+
12
+ export function handleLarkCommand(db: Database, text: string): LarkCard {
13
+ const command = text.trim();
14
+
15
+ if (command === "/status") {
16
+ return {
17
+ type: "card",
18
+ title: "MAESTRO Status",
19
+ fields: {
20
+ workspaces: count(db, "workspace"),
21
+ projects: count(db, "project"),
22
+ open_tasks: countTasksByPredicate(db, (task) => isInitialTaskStatus(db, task)),
23
+ working_agents: countWhere(db, "agent", "status = 'working'"),
24
+ agent_runtimes: count(db, "agent_runtime"),
25
+ unread_inbox: countWhere(db, "inbox_message", "status = 'unread'"),
26
+ },
27
+ };
28
+ }
29
+
30
+ if (command === "/inbox") {
31
+ return {
32
+ type: "card",
33
+ title: "MAESTRO Inbox",
34
+ items: db.query("SELECT * FROM inbox_message WHERE to_actor = 'user' ORDER BY created_at DESC LIMIT 10").all() as any[],
35
+ };
36
+ }
37
+
38
+ if (command === "/workspaces") {
39
+ const workspaces = db.query("SELECT id, name, status FROM workspace WHERE status = 'active' ORDER BY created_at DESC LIMIT 10").all() as any[];
40
+ return {
41
+ type: "card",
42
+ title: "Active Workspaces",
43
+ items: workspaces.map(m => ({ body: `${m.name} (${m.id}) — ${m.status}` })),
44
+ };
45
+ }
46
+
47
+ if (command.startsWith("/agent ")) {
48
+ const query = command.slice(7).trim();
49
+ const agent = db.query("SELECT * FROM agent WHERE id = ? OR name LIKE ?").get(query, `%${query}%`) as any;
50
+ if (!agent) return { type: "card", title: "Agent Not Found", items: [{ body: `No agent matching: ${query}` }] };
51
+ return {
52
+ type: "card",
53
+ title: `Agent: ${agent.name}`,
54
+ fields: { status: agent.status, role_id: agent.role_id || "none", runtime: agent.runtime_id || "none" },
55
+ };
56
+ }
57
+
58
+ if (command === "/tasks" || command.startsWith("/tasks ")) {
59
+ const tasks = (db.query("SELECT id, title, status FROM task ORDER BY priority DESC, created_at ASC").all() as any[])
60
+ .filter((task) => isActiveTaskStatus(db, task))
61
+ .slice(0, 10);
62
+ return {
63
+ type: "card",
64
+ title: "Tasks",
65
+ items: tasks.map(t => ({ body: `[${t.status}] ${t.title} (${t.id})` })),
66
+ };
67
+ }
68
+
69
+ return {
70
+ type: "card",
71
+ title: "MAESTRO",
72
+ items: [{ body: "Commands: /status, /inbox, /workspaces, /agent <name>, /tasks" }],
73
+ };
74
+ }
75
+
76
+ export interface LarkWebhookOptions {
77
+ verificationToken?: string;
78
+ }
79
+
80
+ export async function handleLarkWebhook(db: Database, payload: any, opts: LarkWebhookOptions = {}): Promise<any> {
81
+ if (opts.verificationToken && payload.token && payload.token !== opts.verificationToken) {
82
+ return { error: "Invalid Lark verification token" };
83
+ }
84
+
85
+ if (payload.type === "url_verification") {
86
+ return { challenge: payload.challenge };
87
+ }
88
+
89
+ const text = extractLarkText(payload);
90
+ return handleLarkCommand(db, text);
91
+ }
92
+
93
+ export async function verifyLarkSignature(input: {
94
+ timestamp: string;
95
+ signature: string;
96
+ body: string;
97
+ secret: string;
98
+ }): Promise<boolean> {
99
+ const key = await crypto.subtle.importKey(
100
+ "raw",
101
+ new TextEncoder().encode(`${input.timestamp}\n${input.secret}`),
102
+ { name: "HMAC", hash: "SHA-256" },
103
+ false,
104
+ ["sign"],
105
+ );
106
+ const digest = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(input.body));
107
+ const expected = Buffer.from(digest).toString("base64");
108
+ return timingSafeEqual(expected, input.signature);
109
+ }
110
+
111
+ export function handleLarkBatchInbox(db: Database, input: {
112
+ action: "read" | "reply";
113
+ ids: string[];
114
+ actor?: string;
115
+ body?: string;
116
+ }) {
117
+ let updated = 0;
118
+ const ts = now();
119
+ for (const id of input.ids || []) {
120
+ const message = db.query("SELECT * FROM inbox_message WHERE id = ?").get(id) as any;
121
+ if (!message) continue;
122
+ if (input.action === "read") {
123
+ db.run("UPDATE inbox_message SET status = 'read', read_at = ? WHERE id = ?", [ts, id]);
124
+ updated++;
125
+ }
126
+ if (input.action === "reply") {
127
+ db.run(
128
+ `INSERT INTO inbox_message (id, kind, from_actor, to_actor, subject, body, ref_json, status, created_at)
129
+ VALUES (?, 'reply', ?, ?, ?, ?, ?, 'unread', ?)`,
130
+ [
131
+ generateId("msg"),
132
+ input.actor || "user",
133
+ message.from_actor,
134
+ message.subject,
135
+ input.body || "",
136
+ JSON.stringify({ reply_to: id }),
137
+ ts,
138
+ ],
139
+ );
140
+ db.run("UPDATE inbox_message SET status = 'replied' WHERE id = ?", [id]);
141
+ updated++;
142
+ }
143
+ }
144
+ return { updated };
145
+ }
146
+
147
+ export function buildLarkNotification(message: { kind: string; subject?: string; body?: string }) {
148
+ return {
149
+ msg_type: "interactive",
150
+ card: {
151
+ header: {
152
+ title: { tag: "plain_text", content: message.subject || `MAESTRO ${message.kind}` },
153
+ },
154
+ elements: [
155
+ {
156
+ tag: "div",
157
+ text: { tag: "lark_md", content: message.body || "" },
158
+ },
159
+ ],
160
+ },
161
+ };
162
+ }
163
+
164
+ export async function sendLarkNotification(input: {
165
+ webhook_url: string;
166
+ kind: string;
167
+ subject?: string;
168
+ body?: string;
169
+ fetchImpl?: typeof fetch;
170
+ }) {
171
+ const payload = buildLarkNotification(input);
172
+ const fetcher = input.fetchImpl || fetch;
173
+ const res = await fetcher(input.webhook_url, {
174
+ method: "POST",
175
+ headers: { "Content-Type": "application/json" },
176
+ body: JSON.stringify(payload),
177
+ });
178
+ const response = await res.json().catch(() => ({}));
179
+ return { delivered: res.ok, status: res.status, response, payload };
180
+ }
181
+
182
+ function extractLarkText(payload: any): string {
183
+ const content = payload?.event?.message?.content;
184
+ if (typeof content === "string") {
185
+ try {
186
+ return JSON.parse(content).text || "";
187
+ } catch {
188
+ return content;
189
+ }
190
+ }
191
+ return payload.text || "";
192
+ }
193
+
194
+ function timingSafeEqual(a: string, b: string): boolean {
195
+ const left = Buffer.from(a);
196
+ const right = Buffer.from(b);
197
+ if (left.length !== right.length) return false;
198
+ return crypto.subtle ? constantTimeCompare(left, right) : a === b;
199
+ }
200
+
201
+ function constantTimeCompare(left: Buffer, right: Buffer): boolean {
202
+ let diff = 0;
203
+ for (let i = 0; i < left.length; i++) diff |= left[i] ^ right[i];
204
+ return diff === 0;
205
+ }
206
+
207
+ function count(db: Database, table: string): number {
208
+ return (db.query(`SELECT COUNT(*) AS count FROM ${table}`).get() as any).count;
209
+ }
210
+
211
+ function countWhere(db: Database, table: string, where: string): number {
212
+ return (db.query(`SELECT COUNT(*) AS count FROM ${table} WHERE ${where}`).get() as any).count;
213
+ }
@@ -0,0 +1,273 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { generateId, now } from "../../core/db";
3
+ import { countTasksByPredicate, isActiveTaskStatus, isInitialTaskStatus } from "../../workflows";
4
+
5
+ export interface TelegramApiCall {
6
+ method: "sendMessage";
7
+ payload: {
8
+ chat_id: number | string;
9
+ text: string;
10
+ parse_mode?: "Markdown";
11
+ disable_web_page_preview?: boolean;
12
+ };
13
+ }
14
+
15
+ export interface TelegramBridgeResult {
16
+ response?: TelegramApiCall;
17
+ bridged?: boolean; // true if message was forwarded to workspace chat
18
+ }
19
+
20
+ export function handleTelegramCommand(db: Database, text: string, chatId: number | string): TelegramApiCall {
21
+ const command = text.trim();
22
+
23
+ // /bind <workspace_name> — 绑定当前 group 到 workspace
24
+ if (command.startsWith("/bind ")) {
25
+ const wsName = command.slice(6).trim();
26
+ if (!wsName) {
27
+ return { method: "sendMessage", payload: buildTelegramMessage({ chat_id: chatId, subject: "Bind", body: "Usage: /bind <workspace_name>" }) };
28
+ }
29
+ const workspace = db.query("SELECT id, name FROM workspace WHERE LOWER(name) = LOWER(?) AND status = 'active'").get(wsName) as any;
30
+ if (!workspace) {
31
+ return { method: "sendMessage", payload: buildTelegramMessage({ chat_id: chatId, subject: "Bind Failed", body: `Workspace not found: ${wsName}` }) };
32
+ }
33
+ const ts = now();
34
+ db.run(
35
+ "INSERT OR REPLACE INTO telegram_binding (chat_id, workspace_id, bound_at) VALUES (?, ?, ?)",
36
+ [String(chatId), workspace.id, ts]
37
+ );
38
+ return { method: "sendMessage", payload: buildTelegramMessage({ chat_id: chatId, subject: "Bound", body: `This group is now linked to workspace: ${workspace.name}\n\n⚠️ To receive all messages, either:\n• Promote the bot to Group Admin, or\n• Disable Group Privacy via @BotFather (Bot Settings → Group Privacy → Turn off), then remove and re-add the bot` }) };
39
+ }
40
+
41
+ // /unbind — 解除绑定
42
+ if (command === "/unbind") {
43
+ db.run("DELETE FROM telegram_binding WHERE chat_id = ?", [String(chatId)]);
44
+ return { method: "sendMessage", payload: buildTelegramMessage({ chat_id: chatId, subject: "Unbound", body: "This group is no longer linked to any workspace." }) };
45
+ }
46
+
47
+ if (command === "/status") {
48
+ return {
49
+ method: "sendMessage",
50
+ payload: buildTelegramMessage({
51
+ chat_id: chatId,
52
+ subject: "MAESTRO Status",
53
+ body: [
54
+ `workspaces: ${count(db, "workspace")}`,
55
+ `projects: ${count(db, "project")}`,
56
+ `open_tasks: ${countTasksByPredicate(db, (task) => isInitialTaskStatus(db, task))}`,
57
+ `working_agents: ${countWhere(db, "agent", "status = 'working'")}`,
58
+ `agent_runtimes: ${count(db, "agent_runtime")}`,
59
+ `unread_inbox: ${countWhere(db, "inbox_message", "status = 'unread'")}`,
60
+ ].join("\n"),
61
+ }),
62
+ };
63
+ }
64
+
65
+ if (command === "/inbox") {
66
+ const rows = db.query("SELECT * FROM inbox_message WHERE to_actor = 'user' ORDER BY created_at DESC LIMIT 10").all() as any[];
67
+ return {
68
+ method: "sendMessage",
69
+ payload: buildTelegramMessage({
70
+ chat_id: chatId,
71
+ subject: "MAESTRO Inbox",
72
+ body: rows.length === 0
73
+ ? "No inbox messages."
74
+ : rows.map((row) => `- ${row.subject || row.kind}: ${row.body || ""}`).join("\n"),
75
+ }),
76
+ };
77
+ }
78
+
79
+ if (command === "/workspaces") {
80
+ const workspaces = db.query("SELECT id, name, status FROM workspace WHERE status = 'active' ORDER BY created_at DESC LIMIT 10").all() as any[];
81
+ return {
82
+ method: "sendMessage",
83
+ payload: buildTelegramMessage({
84
+ chat_id: chatId,
85
+ subject: "Active Workspaces",
86
+ body: workspaces.length === 0
87
+ ? "No active workspaces."
88
+ : workspaces.map(m => `- ${m.name} (${m.id}) — ${m.status}`).join("\n"),
89
+ }),
90
+ };
91
+ }
92
+
93
+ if (command.startsWith("/agent ")) {
94
+ const query = command.slice(7).trim();
95
+ const agent = db.query("SELECT * FROM agent WHERE id = ? OR name LIKE ?").get(query, `%${query}%`) as any;
96
+ if (!agent) {
97
+ return {
98
+ method: "sendMessage",
99
+ payload: buildTelegramMessage({
100
+ chat_id: chatId,
101
+ subject: "Agent Not Found",
102
+ body: `No agent matching: ${query}`,
103
+ }),
104
+ };
105
+ }
106
+ return {
107
+ method: "sendMessage",
108
+ payload: buildTelegramMessage({
109
+ chat_id: chatId,
110
+ subject: `Agent: ${agent.name}`,
111
+ body: `status: ${agent.status}\nrole_id: ${agent.role_id || "none"}\nruntime: ${agent.runtime_id || "none"}`,
112
+ }),
113
+ };
114
+ }
115
+
116
+ if (command === "/tasks" || command.startsWith("/tasks ")) {
117
+ const tasks = (db.query("SELECT id, title, status FROM task ORDER BY priority DESC, created_at ASC").all() as any[])
118
+ .filter((task) => isActiveTaskStatus(db, task))
119
+ .slice(0, 10);
120
+ return {
121
+ method: "sendMessage",
122
+ payload: buildTelegramMessage({
123
+ chat_id: chatId,
124
+ subject: "Tasks",
125
+ body: tasks.length === 0
126
+ ? "No active tasks."
127
+ : tasks.map(t => `[${t.status}] ${t.title} (${t.id})`).join("\n"),
128
+ }),
129
+ };
130
+ }
131
+
132
+ return {
133
+ method: "sendMessage",
134
+ payload: buildTelegramMessage({
135
+ chat_id: chatId,
136
+ subject: "MAESTRO",
137
+ body: "Commands: /bind <workspace>, /unbind, /status, /inbox, /workspaces, /agent <name>, /tasks",
138
+ }),
139
+ };
140
+ }
141
+
142
+ export function handleTelegramWebhook(
143
+ db: Database,
144
+ payload: any,
145
+ opts: { secretToken?: string; requestToken?: string } = {},
146
+ ) {
147
+ if (opts.secretToken && opts.requestToken !== opts.secretToken) {
148
+ return { ok: false, error: "Invalid Telegram secret token" };
149
+ }
150
+ const chatId = payload?.message?.chat?.id || payload?.edited_message?.chat?.id;
151
+ const text = payload?.message?.text || payload?.edited_message?.text || "";
152
+ if (!chatId) return { ok: false, error: "Missing Telegram chat id" };
153
+ return { ok: true, response: handleTelegramCommand(db, text, chatId) };
154
+ }
155
+
156
+ export function handleTelegramBatchInbox(db: Database, input: {
157
+ action: "read" | "reply";
158
+ ids: string[];
159
+ actor?: string;
160
+ body?: string;
161
+ }) {
162
+ let updated = 0;
163
+ const ts = now();
164
+ for (const id of input.ids || []) {
165
+ const message = db.query("SELECT * FROM inbox_message WHERE id = ?").get(id) as any;
166
+ if (!message) continue;
167
+ if (input.action === "read") {
168
+ db.run("UPDATE inbox_message SET status = 'read', read_at = ? WHERE id = ?", [ts, id]);
169
+ updated++;
170
+ }
171
+ if (input.action === "reply") {
172
+ db.run(
173
+ `INSERT INTO inbox_message (id, kind, from_actor, to_actor, subject, body, ref_json, status, created_at)
174
+ VALUES (?, 'reply', ?, ?, ?, ?, ?, 'unread', ?)`,
175
+ [
176
+ generateId("msg"),
177
+ input.actor || "user",
178
+ message.from_actor,
179
+ message.subject,
180
+ input.body || "",
181
+ JSON.stringify({ reply_to: id }),
182
+ ts,
183
+ ],
184
+ );
185
+ db.run("UPDATE inbox_message SET status = 'replied' WHERE id = ?", [id]);
186
+ updated++;
187
+ }
188
+ }
189
+ return { updated };
190
+ }
191
+
192
+ export function buildTelegramMessage(input: { chat_id: number | string; subject?: string; body?: string }) {
193
+ return {
194
+ chat_id: input.chat_id,
195
+ text: `*${escapeMarkdown(input.subject || "MAESTRO")}*\n${escapeMarkdown(input.body || "")}`,
196
+ parse_mode: "Markdown" as const,
197
+ disable_web_page_preview: true,
198
+ };
199
+ }
200
+
201
+ export async function sendTelegramNotification(input: {
202
+ bot_token: string;
203
+ chat_id: number | string;
204
+ subject?: string;
205
+ body?: string;
206
+ api_base?: string;
207
+ fetchImpl?: typeof fetch;
208
+ }) {
209
+ const payload = buildTelegramMessage(input);
210
+ const base = (input.api_base || "https://api.telegram.org").replace(/\/$/, "");
211
+ const fetcher = input.fetchImpl || fetch;
212
+ const res = await fetcher(`${base}/bot${input.bot_token}/sendMessage`, {
213
+ method: "POST",
214
+ headers: { "Content-Type": "application/json" },
215
+ body: JSON.stringify(payload),
216
+ });
217
+ const response = await res.json().catch(() => ({}));
218
+ return { delivered: res.ok && response.ok !== false, status: res.status, response, payload };
219
+ }
220
+
221
+ function escapeMarkdown(text: string): string {
222
+ return text.replace(/([_*[\]()~`>#+\-=|{}.!])/g, "\\$1");
223
+ }
224
+
225
+ function count(db: Database, table: string): number {
226
+ return (db.query(`SELECT COUNT(*) AS count FROM ${table}`).get() as any).count;
227
+ }
228
+
229
+ function countWhere(db: Database, table: string, where: string): number {
230
+ return (db.query(`SELECT COUNT(*) AS count FROM ${table} WHERE ${where}`).get() as any).count;
231
+ }
232
+
233
+ // ─── Workspace Chat Bridge ───────────────────────────────────────────────────
234
+
235
+ /**
236
+ * Look up workspace binding for a Telegram chat.
237
+ */
238
+ export function getBindingForChat(db: Database, chatId: number | string): { workspace_id: string } | null {
239
+ return db.query("SELECT workspace_id FROM telegram_binding WHERE chat_id = ?").get(String(chatId)) as any || null;
240
+ }
241
+
242
+ /**
243
+ * Check if a message is a command (starts with /).
244
+ */
245
+ export function isTelegramCommand(text: string): boolean {
246
+ return text.trim().startsWith("/");
247
+ }
248
+
249
+ /**
250
+ * Push an agent reply back to the bound TG group.
251
+ */
252
+ export async function pushAgentReplyToTelegram(input: {
253
+ bot_token: string;
254
+ chat_id: string;
255
+ sender_id: string;
256
+ content: string;
257
+ api_base?: string;
258
+ fetchImpl?: typeof fetch;
259
+ }) {
260
+ const text = `🤖 *${escapeMarkdown(input.sender_id)}*\n${escapeMarkdown(input.content)}`;
261
+ const base = (input.api_base || "https://api.telegram.org").replace(/\/$/, "");
262
+ const fetcher = input.fetchImpl || fetch;
263
+ await fetcher(`${base}/bot${input.bot_token}/sendMessage`, {
264
+ method: "POST",
265
+ headers: { "Content-Type": "application/json" },
266
+ body: JSON.stringify({
267
+ chat_id: input.chat_id,
268
+ text,
269
+ parse_mode: "Markdown",
270
+ disable_web_page_preview: true,
271
+ }),
272
+ }).catch(() => {});
273
+ }