@songsid/agend 0.0.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 (232) hide show
  1. package/README.md +210 -0
  2. package/README.zh-TW.md +134 -0
  3. package/dist/access-path.d.ts +10 -0
  4. package/dist/access-path.js +32 -0
  5. package/dist/access-path.js.map +1 -0
  6. package/dist/adapter-world.d.ts +25 -0
  7. package/dist/adapter-world.js +41 -0
  8. package/dist/adapter-world.js.map +1 -0
  9. package/dist/agent-cli-instructions.md +50 -0
  10. package/dist/agent-cli.d.ts +2 -0
  11. package/dist/agent-cli.js +200 -0
  12. package/dist/agent-cli.js.map +1 -0
  13. package/dist/agent-endpoint.d.ts +25 -0
  14. package/dist/agent-endpoint.js +162 -0
  15. package/dist/agent-endpoint.js.map +1 -0
  16. package/dist/backend/antigravity.d.ts +17 -0
  17. package/dist/backend/antigravity.js +98 -0
  18. package/dist/backend/antigravity.js.map +1 -0
  19. package/dist/backend/claude-code.d.ts +23 -0
  20. package/dist/backend/claude-code.js +171 -0
  21. package/dist/backend/claude-code.js.map +1 -0
  22. package/dist/backend/codex.d.ts +18 -0
  23. package/dist/backend/codex.js +160 -0
  24. package/dist/backend/codex.js.map +1 -0
  25. package/dist/backend/factory.d.ts +2 -0
  26. package/dist/backend/factory.js +28 -0
  27. package/dist/backend/factory.js.map +1 -0
  28. package/dist/backend/gemini-cli.d.ts +17 -0
  29. package/dist/backend/gemini-cli.js +163 -0
  30. package/dist/backend/gemini-cli.js.map +1 -0
  31. package/dist/backend/index.d.ts +7 -0
  32. package/dist/backend/index.js +7 -0
  33. package/dist/backend/index.js.map +1 -0
  34. package/dist/backend/kiro.d.ts +17 -0
  35. package/dist/backend/kiro.js +147 -0
  36. package/dist/backend/kiro.js.map +1 -0
  37. package/dist/backend/marker-utils.d.ts +13 -0
  38. package/dist/backend/marker-utils.js +64 -0
  39. package/dist/backend/marker-utils.js.map +1 -0
  40. package/dist/backend/mock.d.ts +25 -0
  41. package/dist/backend/mock.js +85 -0
  42. package/dist/backend/mock.js.map +1 -0
  43. package/dist/backend/opencode.d.ts +16 -0
  44. package/dist/backend/opencode.js +136 -0
  45. package/dist/backend/opencode.js.map +1 -0
  46. package/dist/backend/types.d.ts +86 -0
  47. package/dist/backend/types.js +33 -0
  48. package/dist/backend/types.js.map +1 -0
  49. package/dist/channel/access-manager.d.ts +18 -0
  50. package/dist/channel/access-manager.js +153 -0
  51. package/dist/channel/access-manager.js.map +1 -0
  52. package/dist/channel/adapters/telegram.d.ts +63 -0
  53. package/dist/channel/adapters/telegram.js +646 -0
  54. package/dist/channel/adapters/telegram.js.map +1 -0
  55. package/dist/channel/attachment-handler.d.ts +15 -0
  56. package/dist/channel/attachment-handler.js +88 -0
  57. package/dist/channel/attachment-handler.js.map +1 -0
  58. package/dist/channel/factory.d.ts +12 -0
  59. package/dist/channel/factory.js +67 -0
  60. package/dist/channel/factory.js.map +1 -0
  61. package/dist/channel/ipc-bridge.d.ts +26 -0
  62. package/dist/channel/ipc-bridge.js +220 -0
  63. package/dist/channel/ipc-bridge.js.map +1 -0
  64. package/dist/channel/mcp-server.d.ts +10 -0
  65. package/dist/channel/mcp-server.js +288 -0
  66. package/dist/channel/mcp-server.js.map +1 -0
  67. package/dist/channel/mcp-tools.d.ts +17 -0
  68. package/dist/channel/mcp-tools.js +110 -0
  69. package/dist/channel/mcp-tools.js.map +1 -0
  70. package/dist/channel/message-bus.d.ts +17 -0
  71. package/dist/channel/message-bus.js +86 -0
  72. package/dist/channel/message-bus.js.map +1 -0
  73. package/dist/channel/message-queue.d.ts +39 -0
  74. package/dist/channel/message-queue.js +253 -0
  75. package/dist/channel/message-queue.js.map +1 -0
  76. package/dist/channel/tool-router.d.ts +6 -0
  77. package/dist/channel/tool-router.js +75 -0
  78. package/dist/channel/tool-router.js.map +1 -0
  79. package/dist/channel/tool-tracker.d.ts +13 -0
  80. package/dist/channel/tool-tracker.js +58 -0
  81. package/dist/channel/tool-tracker.js.map +1 -0
  82. package/dist/channel/types.d.ts +118 -0
  83. package/dist/channel/types.js +2 -0
  84. package/dist/channel/types.js.map +1 -0
  85. package/dist/chat-export.d.ts +4 -0
  86. package/dist/chat-export.js +91 -0
  87. package/dist/chat-export.js.map +1 -0
  88. package/dist/classic-channel-manager.d.ts +59 -0
  89. package/dist/classic-channel-manager.js +193 -0
  90. package/dist/classic-channel-manager.js.map +1 -0
  91. package/dist/cli.d.ts +2 -0
  92. package/dist/cli.js +1833 -0
  93. package/dist/cli.js.map +1 -0
  94. package/dist/config.d.ts +9 -0
  95. package/dist/config.js +118 -0
  96. package/dist/config.js.map +1 -0
  97. package/dist/context-guardian.d.ts +26 -0
  98. package/dist/context-guardian.js +73 -0
  99. package/dist/context-guardian.js.map +1 -0
  100. package/dist/cost-guard.d.ts +36 -0
  101. package/dist/cost-guard.js +147 -0
  102. package/dist/cost-guard.js.map +1 -0
  103. package/dist/daemon-entry.d.ts +1 -0
  104. package/dist/daemon-entry.js +29 -0
  105. package/dist/daemon-entry.js.map +1 -0
  106. package/dist/daemon.d.ts +152 -0
  107. package/dist/daemon.js +1714 -0
  108. package/dist/daemon.js.map +1 -0
  109. package/dist/daily-summary.d.ts +13 -0
  110. package/dist/daily-summary.js +55 -0
  111. package/dist/daily-summary.js.map +1 -0
  112. package/dist/event-log.d.ts +36 -0
  113. package/dist/event-log.js +100 -0
  114. package/dist/event-log.js.map +1 -0
  115. package/dist/export-import.d.ts +2 -0
  116. package/dist/export-import.js +162 -0
  117. package/dist/export-import.js.map +1 -0
  118. package/dist/fleet-context.d.ts +61 -0
  119. package/dist/fleet-context.js +4 -0
  120. package/dist/fleet-context.js.map +1 -0
  121. package/dist/fleet-dashboard-html.d.ts +6 -0
  122. package/dist/fleet-dashboard-html.js +443 -0
  123. package/dist/fleet-dashboard-html.js.map +1 -0
  124. package/dist/fleet-health-server.d.ts +35 -0
  125. package/dist/fleet-health-server.js +290 -0
  126. package/dist/fleet-health-server.js.map +1 -0
  127. package/dist/fleet-instructions.d.ts +5 -0
  128. package/dist/fleet-instructions.js +161 -0
  129. package/dist/fleet-instructions.js.map +1 -0
  130. package/dist/fleet-manager.d.ts +212 -0
  131. package/dist/fleet-manager.js +3655 -0
  132. package/dist/fleet-manager.js.map +1 -0
  133. package/dist/fleet-rpc-handlers.d.ts +42 -0
  134. package/dist/fleet-rpc-handlers.js +356 -0
  135. package/dist/fleet-rpc-handlers.js.map +1 -0
  136. package/dist/fleet-system-prompt.d.ts +11 -0
  137. package/dist/fleet-system-prompt.js +61 -0
  138. package/dist/fleet-system-prompt.js.map +1 -0
  139. package/dist/general-knowledge/skills.md +177 -0
  140. package/dist/hang-detector.d.ts +16 -0
  141. package/dist/hang-detector.js +53 -0
  142. package/dist/hang-detector.js.map +1 -0
  143. package/dist/index.d.ts +8 -0
  144. package/dist/index.js +6 -0
  145. package/dist/index.js.map +1 -0
  146. package/dist/instance-lifecycle.d.ts +90 -0
  147. package/dist/instance-lifecycle.js +592 -0
  148. package/dist/instance-lifecycle.js.map +1 -0
  149. package/dist/instructions.d.ts +15 -0
  150. package/dist/instructions.js +90 -0
  151. package/dist/instructions.js.map +1 -0
  152. package/dist/logger.d.ts +7 -0
  153. package/dist/logger.js +84 -0
  154. package/dist/logger.js.map +1 -0
  155. package/dist/outbound-handlers.d.ts +51 -0
  156. package/dist/outbound-handlers.js +739 -0
  157. package/dist/outbound-handlers.js.map +1 -0
  158. package/dist/outbound-schemas.d.ts +238 -0
  159. package/dist/outbound-schemas.js +248 -0
  160. package/dist/outbound-schemas.js.map +1 -0
  161. package/dist/paths.d.ts +10 -0
  162. package/dist/paths.js +42 -0
  163. package/dist/paths.js.map +1 -0
  164. package/dist/plugin/agend/.claude-plugin/plugin.json +5 -0
  165. package/dist/quickstart.d.ts +1 -0
  166. package/dist/quickstart.js +595 -0
  167. package/dist/quickstart.js.map +1 -0
  168. package/dist/routing-engine.d.ts +22 -0
  169. package/dist/routing-engine.js +44 -0
  170. package/dist/routing-engine.js.map +1 -0
  171. package/dist/safe-async.d.ts +6 -0
  172. package/dist/safe-async.js +20 -0
  173. package/dist/safe-async.js.map +1 -0
  174. package/dist/scheduler/db.d.ts +37 -0
  175. package/dist/scheduler/db.js +360 -0
  176. package/dist/scheduler/db.js.map +1 -0
  177. package/dist/scheduler/db.test.d.ts +1 -0
  178. package/dist/scheduler/db.test.js +92 -0
  179. package/dist/scheduler/db.test.js.map +1 -0
  180. package/dist/scheduler/index.d.ts +4 -0
  181. package/dist/scheduler/index.js +4 -0
  182. package/dist/scheduler/index.js.map +1 -0
  183. package/dist/scheduler/scheduler.d.ts +44 -0
  184. package/dist/scheduler/scheduler.js +197 -0
  185. package/dist/scheduler/scheduler.js.map +1 -0
  186. package/dist/scheduler/scheduler.test.d.ts +1 -0
  187. package/dist/scheduler/scheduler.test.js +119 -0
  188. package/dist/scheduler/scheduler.test.js.map +1 -0
  189. package/dist/scheduler/types.d.ts +107 -0
  190. package/dist/scheduler/types.js +7 -0
  191. package/dist/scheduler/types.js.map +1 -0
  192. package/dist/service-installer.d.ts +17 -0
  193. package/dist/service-installer.js +182 -0
  194. package/dist/service-installer.js.map +1 -0
  195. package/dist/setup-wizard.d.ts +48 -0
  196. package/dist/setup-wizard.js +701 -0
  197. package/dist/setup-wizard.js.map +1 -0
  198. package/dist/statusline-watcher.d.ts +34 -0
  199. package/dist/statusline-watcher.js +73 -0
  200. package/dist/statusline-watcher.js.map +1 -0
  201. package/dist/stt.d.ts +10 -0
  202. package/dist/stt.js +33 -0
  203. package/dist/stt.js.map +1 -0
  204. package/dist/tmux-control.d.ts +52 -0
  205. package/dist/tmux-control.js +207 -0
  206. package/dist/tmux-control.js.map +1 -0
  207. package/dist/tmux-manager.d.ts +44 -0
  208. package/dist/tmux-manager.js +218 -0
  209. package/dist/tmux-manager.js.map +1 -0
  210. package/dist/topic-archiver.d.ts +40 -0
  211. package/dist/topic-archiver.js +103 -0
  212. package/dist/topic-archiver.js.map +1 -0
  213. package/dist/topic-commands.d.ts +28 -0
  214. package/dist/topic-commands.js +359 -0
  215. package/dist/topic-commands.js.map +1 -0
  216. package/dist/transcript-monitor.d.ts +23 -0
  217. package/dist/transcript-monitor.js +164 -0
  218. package/dist/transcript-monitor.js.map +1 -0
  219. package/dist/types.d.ts +211 -0
  220. package/dist/types.js +2 -0
  221. package/dist/types.js.map +1 -0
  222. package/dist/ui/dashboard.html +719 -0
  223. package/dist/web-api.d.ts +101 -0
  224. package/dist/web-api.js +648 -0
  225. package/dist/web-api.js.map +1 -0
  226. package/dist/webhook-emitter.d.ts +15 -0
  227. package/dist/webhook-emitter.js +41 -0
  228. package/dist/webhook-emitter.js.map +1 -0
  229. package/dist/workflow-templates/default.md +35 -0
  230. package/package.json +76 -0
  231. package/templates/launchd.plist.ejs +31 -0
  232. package/templates/systemd.service.ejs +16 -0
@@ -0,0 +1,648 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { execFileSync } from "node:child_process";
5
+ import { CreateInstanceArgs, validateArgs } from "./outbound-schemas.js";
6
+ import { z } from "zod";
7
+ // ── Strict public-facing schemas ────────────────────────────────────────────
8
+ // web-api endpoints must reject unknown fields so the dashboard cannot inject
9
+ // internal-only flags that would reach handleCreate/scheduler/config writers.
10
+ const MAX_TEXT = 16_384;
11
+ const TaskCreateSchema = z.object({
12
+ title: z.string().min(1).max(512),
13
+ description: z.string().max(MAX_TEXT).optional(),
14
+ priority: z.enum(["low", "normal", "high", "urgent"]).optional(),
15
+ assignee: z.string().max(128).optional(),
16
+ }).strict();
17
+ const TaskUpdateSchema = z.object({
18
+ action: z.enum(["claim", "complete", "update"]).optional(),
19
+ assignee: z.string().max(128).optional(),
20
+ result: z.string().max(MAX_TEXT).optional(),
21
+ status: z.string().max(64).optional(),
22
+ title: z.string().max(512).optional(),
23
+ description: z.string().max(MAX_TEXT).optional(),
24
+ priority: z.string().max(64).optional(),
25
+ }).strict();
26
+ const ScheduleCreateSchema = z.object({
27
+ cron: z.string().min(1).max(256),
28
+ message: z.string().min(1).max(MAX_TEXT),
29
+ target: z.string().min(1).max(256),
30
+ label: z.string().max(256).optional(),
31
+ timezone: z.string().max(128).optional(),
32
+ }).strict();
33
+ const TeamCreateSchema = z.object({
34
+ name: z.string().min(1).max(128).regex(/^[A-Za-z0-9._-]+$/),
35
+ members: z.array(z.string().min(1).max(256)).min(1).max(256),
36
+ description: z.string().max(MAX_TEXT).optional(),
37
+ }).strict();
38
+ const ConfigUpdateSchema = z.object({
39
+ channel: z.object({
40
+ group_id: z.union([z.number(), z.string()]).optional(),
41
+ access: z.record(z.string(), z.unknown()).optional(),
42
+ }).strict().optional(),
43
+ defaults: z.object({
44
+ backend: z.enum(["claude-code", "gemini-cli", "codex", "opencode", "kiro-cli", "antigravity"]).optional(),
45
+ model: z.string().max(128).optional(),
46
+ }).strict().optional(),
47
+ project_roots: z.array(z.string().min(1).max(1024)).max(64).optional(),
48
+ }).strict();
49
+ const SendMessageSchema = z.object({
50
+ instance: z.string().min(1).max(128),
51
+ message: z.string().min(1).max(MAX_TEXT),
52
+ }).strict();
53
+ /**
54
+ * Push a single SSE frame to every client. If a client throws (closed socket
55
+ * after a network drop, etc.), evict it from the set and continue — without
56
+ * the eviction the dead entry leaks forever, and without the try/catch a
57
+ * single dead client breaks delivery to every client iterated after it.
58
+ */
59
+ export function broadcastSseEvent(clients, event, data, onError) {
60
+ const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
61
+ const dead = [];
62
+ for (const client of clients) {
63
+ try {
64
+ client.write(payload);
65
+ }
66
+ catch (err) {
67
+ dead.push(client);
68
+ onError?.(err);
69
+ }
70
+ }
71
+ for (const c of dead) {
72
+ clients.delete(c);
73
+ try {
74
+ c.end();
75
+ }
76
+ catch { /* socket already gone */ }
77
+ }
78
+ }
79
+ function parseOrReject(schema, data, res) {
80
+ const r = schema.safeParse(data);
81
+ if (!r.success) {
82
+ const issue = r.error.issues[0];
83
+ const path = issue.path.join(".");
84
+ json(res, 400, { error: `${path || "body"}: ${issue.message}` });
85
+ return null;
86
+ }
87
+ return r.data;
88
+ }
89
+ const __filename = fileURLToPath(import.meta.url);
90
+ const __dirname = dirname(__filename);
91
+ /** Parse JSON body from request. */
92
+ function parseBody(req) {
93
+ return new Promise((resolve, reject) => {
94
+ let body = "";
95
+ req.on("data", (chunk) => { body += chunk.toString(); });
96
+ req.on("end", () => {
97
+ try {
98
+ resolve(JSON.parse(body));
99
+ }
100
+ catch {
101
+ reject(new Error("Invalid JSON"));
102
+ }
103
+ });
104
+ });
105
+ }
106
+ /** Send JSON response. */
107
+ function json(res, status, data) {
108
+ res.setHeader("Content-Type", "application/json");
109
+ res.writeHead(status);
110
+ res.end(JSON.stringify(data));
111
+ }
112
+ /**
113
+ * Handle a Web UI request. Returns true if handled, false to pass through.
114
+ */
115
+ export function handleWebRequest(req, res, url, ctx) {
116
+ const path = url.pathname;
117
+ const method = req.method ?? "GET";
118
+ // Auth check for all /ui routes
119
+ if (path.startsWith("/ui")) {
120
+ const token = url.searchParams.get("token");
121
+ if (token !== ctx.webToken) {
122
+ json(res, 401, { error: "Unauthorized" });
123
+ return true;
124
+ }
125
+ }
126
+ else {
127
+ return false;
128
+ }
129
+ // ── Static files ───────────────────────────────────────
130
+ if (method === "GET" && path === "/ui") {
131
+ try {
132
+ const html = readFileSync(join(__dirname, "ui", "dashboard.html"), "utf-8");
133
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
134
+ res.writeHead(200);
135
+ res.end(html);
136
+ }
137
+ catch {
138
+ json(res, 500, { error: "dashboard.html not found" });
139
+ }
140
+ return true;
141
+ }
142
+ // Serve JS modules
143
+ if (method === "GET" && path.startsWith("/ui/js/")) {
144
+ const fileName = path.slice("/ui/js/".length);
145
+ if (!/^[a-z0-9_-]+\.js$/.test(fileName)) {
146
+ json(res, 400, { error: "Invalid file name" });
147
+ return true;
148
+ }
149
+ try {
150
+ const js = readFileSync(join(__dirname, "ui", fileName), "utf-8");
151
+ res.setHeader("Content-Type", "application/javascript; charset=utf-8");
152
+ res.writeHead(200);
153
+ res.end(js);
154
+ }
155
+ catch {
156
+ json(res, 404, { error: "File not found" });
157
+ }
158
+ return true;
159
+ }
160
+ // ── Backend detection ─────────────────────────────────
161
+ if (method === "GET" && path === "/ui/backends") {
162
+ const BACKENDS = [
163
+ { name: "claude-code", binary: "claude" },
164
+ { name: "codex", binary: "codex" },
165
+ { name: "gemini-cli", binary: "gemini" },
166
+ { name: "opencode", binary: "opencode" },
167
+ { name: "kiro-cli", binary: "kiro-cli" },
168
+ ];
169
+ const backends = BACKENDS.map(b => {
170
+ let installed = false;
171
+ let binPath = "";
172
+ try {
173
+ binPath = execFileSync("which", [b.binary], { stdio: "pipe" }).toString().trim();
174
+ installed = true;
175
+ }
176
+ catch { /* */ }
177
+ return { name: b.name, binary: b.binary, installed, path: binPath };
178
+ });
179
+ json(res, 200, { backends });
180
+ return true;
181
+ }
182
+ // ── SSE ────────────────────────────────────────────────
183
+ if (method === "GET" && path === "/ui/events") {
184
+ res.writeHead(200, {
185
+ "Content-Type": "text/event-stream",
186
+ "Cache-Control": "no-cache",
187
+ Connection: "keep-alive",
188
+ });
189
+ res.write(`event: status\ndata: ${JSON.stringify(ctx.getUiStatus())}\n\n`);
190
+ ctx.sseClients.add(res);
191
+ const interval = setInterval(() => {
192
+ try {
193
+ res.write(`event: status\ndata: ${JSON.stringify(ctx.getUiStatus())}\n\n`);
194
+ }
195
+ catch {
196
+ cleanup();
197
+ }
198
+ }, 10_000);
199
+ let cleanedUp = false;
200
+ const cleanup = () => {
201
+ if (cleanedUp)
202
+ return;
203
+ cleanedUp = true;
204
+ ctx.sseClients.delete(res);
205
+ clearInterval(interval);
206
+ };
207
+ // `close` covers normal disconnects; `error` covers network resets that
208
+ // never deliver a clean FIN. Without both, dead clients accumulate in
209
+ // sseClients and the heartbeat interval keeps firing forever.
210
+ req.on("close", cleanup);
211
+ req.on("error", cleanup);
212
+ res.on("error", cleanup);
213
+ return true;
214
+ }
215
+ // ── Send message ───────────────────────────────────────
216
+ if (method === "POST" && path === "/ui/send") {
217
+ handleSendMessage(req, res, ctx);
218
+ return true;
219
+ }
220
+ // ── Instance operations ────────────────────────────────
221
+ const stopMatch = path.match(/^\/ui\/stop\/(.+)$/);
222
+ if (method === "POST" && stopMatch) {
223
+ const name = decodeURIComponent(stopMatch[1]);
224
+ if (!ctx.fleetConfig?.instances[name]) {
225
+ json(res, 404, { error: `Instance not found: ${name}` });
226
+ return true;
227
+ }
228
+ (async () => {
229
+ try {
230
+ await ctx.stopInstance(name);
231
+ ctx.emitSseEvent("status", ctx.getUiStatus());
232
+ json(res, 200, { stopped: name });
233
+ }
234
+ catch (err) {
235
+ json(res, 500, { error: err.message });
236
+ }
237
+ })();
238
+ return true;
239
+ }
240
+ const startMatch = path.match(/^\/ui\/start\/(.+)$/);
241
+ if (method === "POST" && startMatch) {
242
+ const name = decodeURIComponent(startMatch[1]);
243
+ const config = ctx.fleetConfig?.instances[name];
244
+ if (!config) {
245
+ json(res, 404, { error: `Instance not found: ${name}` });
246
+ return true;
247
+ }
248
+ const topicMode = ctx.fleetConfig?.channel?.mode === "topic";
249
+ (async () => {
250
+ try {
251
+ await ctx.startInstance(name, config, topicMode ?? false);
252
+ ctx.emitSseEvent("status", ctx.getUiStatus());
253
+ json(res, 200, { started: name });
254
+ }
255
+ catch (err) {
256
+ json(res, 500, { error: err.message });
257
+ }
258
+ })();
259
+ return true;
260
+ }
261
+ const deleteMatch = path.match(/^\/ui\/instances\/(.+)\/delete$/);
262
+ if (method === "POST" && deleteMatch) {
263
+ const name = decodeURIComponent(deleteMatch[1]);
264
+ if (!ctx.fleetConfig?.instances[name]) {
265
+ json(res, 404, { error: `Instance not found: ${name}` });
266
+ return true;
267
+ }
268
+ (async () => {
269
+ try {
270
+ const body = await parseBody(req);
271
+ if (body.confirm !== `delete ${name}`) {
272
+ json(res, 400, { error: `Confirmation required: { "confirm": "delete ${name}" }` });
273
+ return;
274
+ }
275
+ await ctx.removeInstance(name);
276
+ ctx.emitSseEvent("status", ctx.getUiStatus());
277
+ json(res, 200, { deleted: name });
278
+ }
279
+ catch (err) {
280
+ json(res, 500, { error: err.message });
281
+ }
282
+ })();
283
+ return true;
284
+ }
285
+ // ── Instance detail ────────────────────────────────────
286
+ const detailMatch = path.match(/^\/ui\/instance\/(.+)$/);
287
+ if (method === "GET" && detailMatch) {
288
+ const name = decodeURIComponent(detailMatch[1]);
289
+ const config = ctx.fleetConfig?.instances[name];
290
+ if (!config) {
291
+ json(res, 404, { error: `Instance not found: ${name}` });
292
+ return true;
293
+ }
294
+ const statusFile = join(ctx.getInstanceDir(name), "statusline.json");
295
+ let statusline = {};
296
+ try {
297
+ statusline = JSON.parse(readFileSync(statusFile, "utf-8"));
298
+ }
299
+ catch { /* */ }
300
+ const activity = ctx.eventLog?.listActivity({ since: new Date(Date.now() - 3600_000).toISOString(), limit: 50 }) ?? [];
301
+ const instanceActivity = activity.filter(a => a.sender === name || a.receiver === name);
302
+ json(res, 200, {
303
+ name,
304
+ status: ctx.getInstanceStatus(name),
305
+ description: config.description,
306
+ display_name: config.display_name,
307
+ working_directory: config.working_directory,
308
+ statusline,
309
+ recent_activity: instanceActivity.slice(0, 20),
310
+ });
311
+ return true;
312
+ }
313
+ // ── Restart (with auth — unifies /restart/:name) ─────
314
+ const restartMatch = path.match(/^\/ui\/restart\/(.+)$/);
315
+ if (method === "POST" && restartMatch) {
316
+ const name = decodeURIComponent(restartMatch[1]);
317
+ (async () => {
318
+ try {
319
+ await ctx.restartSingleInstance(name);
320
+ ctx.emitSseEvent("status", ctx.getUiStatus());
321
+ json(res, 200, { restarted: name });
322
+ }
323
+ catch (err) {
324
+ const status = err.message.includes("not found") ? 404 : 500;
325
+ json(res, status, { error: err.message });
326
+ }
327
+ })();
328
+ return true;
329
+ }
330
+ // ── Create instance ────────────────────────────────────
331
+ if (method === "POST" && path === "/ui/instances") {
332
+ (async () => {
333
+ try {
334
+ const body = await parseBody(req);
335
+ const v = validateArgs(CreateInstanceArgs, body, "create_instance");
336
+ if (!v.ok) {
337
+ json(res, 400, { error: v.error });
338
+ return;
339
+ }
340
+ let result = null;
341
+ let error;
342
+ await ctx.lifecycle.handleCreate(v.data, (r, e) => { result = r; error = e; });
343
+ if (error) {
344
+ json(res, 400, { error });
345
+ }
346
+ else {
347
+ ctx.emitSseEvent("status", ctx.getUiStatus());
348
+ json(res, 200, result);
349
+ }
350
+ }
351
+ catch (err) {
352
+ json(res, 500, { error: err.message });
353
+ }
354
+ })();
355
+ return true;
356
+ }
357
+ // ── Task board ─────────────────────────────────────────
358
+ if (method === "GET" && path === "/ui/tasks") {
359
+ if (!ctx.scheduler) {
360
+ json(res, 200, { tasks: [] });
361
+ return true;
362
+ }
363
+ const tasks = ctx.scheduler.db.listTasks();
364
+ json(res, 200, { tasks });
365
+ return true;
366
+ }
367
+ if (method === "POST" && path === "/ui/tasks") {
368
+ if (!ctx.scheduler) {
369
+ json(res, 500, { error: "Scheduler not initialized" });
370
+ return true;
371
+ }
372
+ (async () => {
373
+ try {
374
+ const body = await parseBody(req);
375
+ const parsed = parseOrReject(TaskCreateSchema, body, res);
376
+ if (!parsed)
377
+ return;
378
+ const task = ctx.scheduler.db.createTask({
379
+ title: parsed.title,
380
+ description: parsed.description,
381
+ priority: parsed.priority,
382
+ assignee: parsed.assignee,
383
+ created_by: "web-user",
384
+ });
385
+ json(res, 200, task);
386
+ }
387
+ catch (err) {
388
+ json(res, 400, { error: err.message });
389
+ }
390
+ })();
391
+ return true;
392
+ }
393
+ const taskMatch = path.match(/^\/ui\/tasks\/(.+)$/);
394
+ if (method === "POST" && taskMatch) {
395
+ if (!ctx.scheduler) {
396
+ json(res, 500, { error: "Scheduler not initialized" });
397
+ return true;
398
+ }
399
+ const id = decodeURIComponent(taskMatch[1]);
400
+ (async () => {
401
+ try {
402
+ const body = await parseBody(req);
403
+ const parsed = parseOrReject(TaskUpdateSchema, body, res);
404
+ if (!parsed)
405
+ return;
406
+ let result;
407
+ if (parsed.action === "claim") {
408
+ result = ctx.scheduler.db.claimTask(id, parsed.assignee || "web-user");
409
+ }
410
+ else if (parsed.action === "complete") {
411
+ result = ctx.scheduler.db.completeTask(id, parsed.result);
412
+ }
413
+ else {
414
+ // Strip action before passing remaining fields to updateTask
415
+ const { action: _a, ...rest } = parsed;
416
+ result = ctx.scheduler.db.updateTask(id, rest);
417
+ }
418
+ json(res, 200, result);
419
+ }
420
+ catch (err) {
421
+ json(res, 400, { error: err.message });
422
+ }
423
+ })();
424
+ return true;
425
+ }
426
+ // ── Schedules ───────────────────────────────────────────
427
+ if (method === "GET" && path === "/ui/schedules") {
428
+ if (!ctx.scheduler) {
429
+ json(res, 200, { schedules: [] });
430
+ return true;
431
+ }
432
+ json(res, 200, { schedules: ctx.scheduler.list() });
433
+ return true;
434
+ }
435
+ if (method === "POST" && path === "/ui/schedules") {
436
+ if (!ctx.scheduler) {
437
+ json(res, 500, { error: "Scheduler not initialized" });
438
+ return true;
439
+ }
440
+ (async () => {
441
+ try {
442
+ const body = await parseBody(req);
443
+ const parsed = parseOrReject(ScheduleCreateSchema, body, res);
444
+ if (!parsed)
445
+ return;
446
+ const schedule = ctx.scheduler.create(parsed);
447
+ json(res, 200, schedule);
448
+ }
449
+ catch (err) {
450
+ json(res, 400, { error: err.message });
451
+ }
452
+ })();
453
+ return true;
454
+ }
455
+ const schedDelMatch = path.match(/^\/ui\/schedules\/(.+)$/);
456
+ if (method === "DELETE" && schedDelMatch) {
457
+ if (!ctx.scheduler) {
458
+ json(res, 500, { error: "Scheduler not initialized" });
459
+ return true;
460
+ }
461
+ try {
462
+ ctx.scheduler.delete(decodeURIComponent(schedDelMatch[1]));
463
+ json(res, 200, { deleted: true });
464
+ }
465
+ catch (err) {
466
+ json(res, 400, { error: err.message });
467
+ }
468
+ return true;
469
+ }
470
+ // ── Teams ──────────────────────────────────────────────
471
+ if (method === "GET" && path === "/ui/teams") {
472
+ const teams = ctx.fleetConfig?.teams ?? {};
473
+ json(res, 200, { teams });
474
+ return true;
475
+ }
476
+ if (method === "POST" && path === "/ui/teams") {
477
+ (async () => {
478
+ try {
479
+ const body = await parseBody(req);
480
+ const parsed = parseOrReject(TeamCreateSchema, body, res);
481
+ if (!parsed)
482
+ return;
483
+ if (!ctx.fleetConfig) {
484
+ json(res, 500, { error: "No fleet config" });
485
+ return;
486
+ }
487
+ if (!ctx.fleetConfig.teams)
488
+ ctx.fleetConfig.teams = {};
489
+ ctx.fleetConfig.teams[parsed.name] = {
490
+ members: parsed.members,
491
+ description: parsed.description,
492
+ };
493
+ ctx.saveFleetConfig();
494
+ json(res, 200, { created: parsed.name });
495
+ }
496
+ catch (err) {
497
+ json(res, 400, { error: err.message });
498
+ }
499
+ })();
500
+ return true;
501
+ }
502
+ const teamDelMatch = path.match(/^\/ui\/teams\/(.+)$/);
503
+ if (method === "DELETE" && teamDelMatch) {
504
+ const name = decodeURIComponent(teamDelMatch[1]);
505
+ if (!ctx.fleetConfig?.teams?.[name]) {
506
+ json(res, 404, { error: `Team not found: ${name}` });
507
+ return true;
508
+ }
509
+ delete ctx.fleetConfig.teams[name];
510
+ ctx.saveFleetConfig();
511
+ json(res, 200, { deleted: name });
512
+ return true;
513
+ }
514
+ // ── Fleet config (read-only, sanitized) ────────────────
515
+ if (method === "GET" && path === "/ui/config") {
516
+ const config = ctx.fleetConfig;
517
+ if (!config) {
518
+ json(res, 200, {});
519
+ return true;
520
+ }
521
+ const ch = config.channel;
522
+ const defaults = config.defaults;
523
+ json(res, 200, {
524
+ channel: ch ? {
525
+ type: ch.type, mode: config.channel.mode,
526
+ bot_token_env: ch.bot_token_env,
527
+ group_id: config.channel.group_id,
528
+ access: ch.access,
529
+ } : undefined,
530
+ defaults: defaults ? { backend: defaults.backend, model: defaults.model } : undefined,
531
+ project_roots: config.project_roots,
532
+ health_port: config.health_port,
533
+ });
534
+ return true;
535
+ }
536
+ if (method === "POST" && path === "/ui/config") {
537
+ (async () => {
538
+ try {
539
+ const body = await parseBody(req);
540
+ const parsed = parseOrReject(ConfigUpdateSchema, body, res);
541
+ if (!parsed)
542
+ return;
543
+ const config = ctx.fleetConfig;
544
+ if (!config) {
545
+ json(res, 500, { error: "No fleet config" });
546
+ return;
547
+ }
548
+ const ch = config.channel;
549
+ // Update channel settings
550
+ if (parsed.channel && ch) {
551
+ if (parsed.channel.group_id != null)
552
+ config.channel.group_id = parsed.channel.group_id;
553
+ if (parsed.channel.access)
554
+ config.channel.access = parsed.channel.access;
555
+ }
556
+ // Update defaults
557
+ if (parsed.defaults) {
558
+ const d = config.defaults;
559
+ if (parsed.defaults.backend)
560
+ d.backend = parsed.defaults.backend;
561
+ if (parsed.defaults.model)
562
+ d.model = parsed.defaults.model;
563
+ }
564
+ // Update project_roots
565
+ if (parsed.project_roots) {
566
+ config.project_roots = parsed.project_roots;
567
+ }
568
+ ctx.saveFleetConfig();
569
+ const needsRestart = parsed.channel?.group_id != null;
570
+ json(res, 200, { saved: true, needs_restart: needsRestart });
571
+ }
572
+ catch (err) {
573
+ json(res, 400, { error: err.message });
574
+ }
575
+ })();
576
+ return true;
577
+ }
578
+ // Not handled
579
+ json(res, 404, { error: "not found" });
580
+ return true;
581
+ }
582
+ /** Handle POST /ui/send — extracted for readability. */
583
+ function handleSendMessage(req, res, ctx) {
584
+ let body = "";
585
+ let size = 0;
586
+ req.on("data", (chunk) => {
587
+ size += chunk.length;
588
+ if (size > MAX_TEXT * 2) {
589
+ // Stop accumulating early on obviously oversized bodies.
590
+ req.destroy();
591
+ return;
592
+ }
593
+ body += chunk.toString();
594
+ });
595
+ req.on("end", () => {
596
+ try {
597
+ const raw = JSON.parse(body);
598
+ const parsed = SendMessageSchema.safeParse(raw);
599
+ if (!parsed.success) {
600
+ const issue = parsed.error.issues[0];
601
+ json(res, 400, { error: `${issue.path.join(".") || "body"}: ${issue.message}` });
602
+ return;
603
+ }
604
+ const { instance, message } = parsed.data;
605
+ const ipc = ctx.instanceIpcClients.get(instance);
606
+ if (!ipc) {
607
+ json(res, 404, { error: `Instance not found: ${instance}` });
608
+ return;
609
+ }
610
+ const ts = new Date().toISOString();
611
+ // Use real Telegram context so daemon's lastChatId/lastThreadId are set,
612
+ // enabling reply tool even when first message comes from Web UI.
613
+ // Pure Web UI mode (no channel config) leaves these empty — TODO: needs
614
+ // a separate reply path for that case.
615
+ const groupId = ctx.fleetConfig?.channel?.group_id;
616
+ const topicId = ctx.fleetConfig?.instances[instance]?.topic_id;
617
+ ipc.send({
618
+ type: "fleet_inbound",
619
+ content: message,
620
+ targetSession: instance,
621
+ meta: {
622
+ chat_id: groupId ? String(groupId) : "",
623
+ message_id: `web-${Date.now()}`,
624
+ user: "web-user", user_id: "web-user",
625
+ ts,
626
+ thread_id: topicId != null ? String(topicId) : "",
627
+ source: "web",
628
+ },
629
+ });
630
+ ctx.lastInboundUser.set(instance, "web-user");
631
+ ctx.eventLog?.logActivity("message", "web-user", message.slice(0, 200), instance);
632
+ ctx.emitSseEvent("message", { instance, sender: "web-user", text: message, ts });
633
+ // Sync to Telegram/Discord
634
+ const syncAdapter = ctx.getAdapterForInstance?.(instance) ?? ctx.adapter;
635
+ const syncGroupId = ctx.getGroupIdForInstance?.(instance) ?? String(ctx.fleetConfig?.channel?.group_id ?? "");
636
+ if (syncAdapter && syncGroupId) {
637
+ const topicId = ctx.fleetConfig?.instances[instance]?.topic_id;
638
+ const preview = message.length > 500 ? message.slice(0, 500) + " [...]" : message;
639
+ syncAdapter.sendText(syncGroupId, `🌐 web-user: ${preview}`, { threadId: topicId != null ? String(topicId) : undefined }).catch(() => ctx.logger.debug({}, "Web→Channel sync failed"));
640
+ }
641
+ json(res, 200, { sent: true });
642
+ }
643
+ catch {
644
+ json(res, 400, { error: "Invalid JSON" });
645
+ }
646
+ });
647
+ }
648
+ //# sourceMappingURL=web-api.js.map