copilot-hub 0.1.0

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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +215 -0
  3. package/apps/agent-engine/.env.example +41 -0
  4. package/apps/agent-engine/LICENSE +21 -0
  5. package/apps/agent-engine/README.md +57 -0
  6. package/apps/agent-engine/bot-registry.example.json +28 -0
  7. package/apps/agent-engine/capabilities/example/index.js +3 -0
  8. package/apps/agent-engine/capabilities/example/manifest.json +14 -0
  9. package/apps/agent-engine/dist/agent-worker.js +241 -0
  10. package/apps/agent-engine/dist/config.js +225 -0
  11. package/apps/agent-engine/dist/index.js +352 -0
  12. package/apps/agent-engine/dist/test/project-fingerprint.test.js +40 -0
  13. package/apps/agent-engine/dist/test/thread-id.test.js +12 -0
  14. package/apps/agent-engine/package.json +28 -0
  15. package/apps/control-plane/.env.example +25 -0
  16. package/apps/control-plane/README.md +35 -0
  17. package/apps/control-plane/bot-registry.example.json +40 -0
  18. package/apps/control-plane/capabilities/example/index.js +3 -0
  19. package/apps/control-plane/capabilities/example/manifest.json +14 -0
  20. package/apps/control-plane/dist/agent-worker.js +243 -0
  21. package/apps/control-plane/dist/channels/channel-factory.js +21 -0
  22. package/apps/control-plane/dist/channels/hub-ops-commands.js +752 -0
  23. package/apps/control-plane/dist/channels/telegram-channel.js +743 -0
  24. package/apps/control-plane/dist/channels/whatsapp-channel.js +35 -0
  25. package/apps/control-plane/dist/config.js +230 -0
  26. package/apps/control-plane/dist/copilot-hub.js +138 -0
  27. package/apps/control-plane/dist/index.js +349 -0
  28. package/apps/control-plane/dist/kernel/admin-contract.js +51 -0
  29. package/apps/control-plane/dist/test/project-fingerprint.test.js +40 -0
  30. package/apps/control-plane/dist/test/thread-id.test.js +12 -0
  31. package/apps/control-plane/package.json +27 -0
  32. package/package.json +89 -0
  33. package/packages/contracts/README.md +10 -0
  34. package/packages/contracts/dist/control-plane.d.ts +24 -0
  35. package/packages/contracts/dist/control-plane.js +37 -0
  36. package/packages/contracts/dist/control-plane.js.map +1 -0
  37. package/packages/contracts/dist/index.d.ts +1 -0
  38. package/packages/contracts/dist/index.js +2 -0
  39. package/packages/contracts/dist/index.js.map +1 -0
  40. package/packages/contracts/package.json +27 -0
  41. package/packages/core/README.md +33 -0
  42. package/packages/core/dist/agent-supervisor.d.ts +39 -0
  43. package/packages/core/dist/agent-supervisor.js +552 -0
  44. package/packages/core/dist/agent-supervisor.js.map +1 -0
  45. package/packages/core/dist/bot-manager.d.ts +66 -0
  46. package/packages/core/dist/bot-manager.js +333 -0
  47. package/packages/core/dist/bot-manager.js.map +1 -0
  48. package/packages/core/dist/bot-registry.d.ts +60 -0
  49. package/packages/core/dist/bot-registry.js +381 -0
  50. package/packages/core/dist/bot-registry.js.map +1 -0
  51. package/packages/core/dist/bot-runtime.d.ts +135 -0
  52. package/packages/core/dist/bot-runtime.js +349 -0
  53. package/packages/core/dist/bot-runtime.js.map +1 -0
  54. package/packages/core/dist/bridge-service.d.ts +39 -0
  55. package/packages/core/dist/bridge-service.js +272 -0
  56. package/packages/core/dist/bridge-service.js.map +1 -0
  57. package/packages/core/dist/capability-manager.d.ts +18 -0
  58. package/packages/core/dist/capability-manager.js +335 -0
  59. package/packages/core/dist/capability-manager.js.map +1 -0
  60. package/packages/core/dist/capability-scaffold.d.ts +26 -0
  61. package/packages/core/dist/capability-scaffold.js +118 -0
  62. package/packages/core/dist/capability-scaffold.js.map +1 -0
  63. package/packages/core/dist/channel-factory.d.ts +6 -0
  64. package/packages/core/dist/channel-factory.js +22 -0
  65. package/packages/core/dist/channel-factory.js.map +1 -0
  66. package/packages/core/dist/codex-app-client.d.ts +56 -0
  67. package/packages/core/dist/codex-app-client.js +762 -0
  68. package/packages/core/dist/codex-app-client.js.map +1 -0
  69. package/packages/core/dist/codex-provider.d.ts +31 -0
  70. package/packages/core/dist/codex-provider.js +64 -0
  71. package/packages/core/dist/codex-provider.js.map +1 -0
  72. package/packages/core/dist/control-permission.d.ts +19 -0
  73. package/packages/core/dist/control-permission.js +106 -0
  74. package/packages/core/dist/control-permission.js.map +1 -0
  75. package/packages/core/dist/control-plane-actions.d.ts +1 -0
  76. package/packages/core/dist/control-plane-actions.js +2 -0
  77. package/packages/core/dist/control-plane-actions.js.map +1 -0
  78. package/packages/core/dist/example-capability.d.ts +17 -0
  79. package/packages/core/dist/example-capability.js +22 -0
  80. package/packages/core/dist/example-capability.js.map +1 -0
  81. package/packages/core/dist/extension-contract.d.ts +22 -0
  82. package/packages/core/dist/extension-contract.js +28 -0
  83. package/packages/core/dist/extension-contract.js.map +1 -0
  84. package/packages/core/dist/index.d.ts +26 -0
  85. package/packages/core/dist/index.js +27 -0
  86. package/packages/core/dist/index.js.map +1 -0
  87. package/packages/core/dist/instance-lock.d.ts +9 -0
  88. package/packages/core/dist/instance-lock.js +74 -0
  89. package/packages/core/dist/instance-lock.js.map +1 -0
  90. package/packages/core/dist/kernel-control-plane.d.ts +16 -0
  91. package/packages/core/dist/kernel-control-plane.js +500 -0
  92. package/packages/core/dist/kernel-control-plane.js.map +1 -0
  93. package/packages/core/dist/kernel-version.d.ts +1 -0
  94. package/packages/core/dist/kernel-version.js +2 -0
  95. package/packages/core/dist/kernel-version.js.map +1 -0
  96. package/packages/core/dist/project-fingerprint.d.ts +11 -0
  97. package/packages/core/dist/project-fingerprint.js +33 -0
  98. package/packages/core/dist/project-fingerprint.js.map +1 -0
  99. package/packages/core/dist/provider-factory.d.ts +7 -0
  100. package/packages/core/dist/provider-factory.js +21 -0
  101. package/packages/core/dist/provider-factory.js.map +1 -0
  102. package/packages/core/dist/secret-store.d.ts +18 -0
  103. package/packages/core/dist/secret-store.js +110 -0
  104. package/packages/core/dist/secret-store.js.map +1 -0
  105. package/packages/core/dist/state-store.d.ts +50 -0
  106. package/packages/core/dist/state-store.js +324 -0
  107. package/packages/core/dist/state-store.js.map +1 -0
  108. package/packages/core/dist/telegram-channel.d.ts +27 -0
  109. package/packages/core/dist/telegram-channel.js +951 -0
  110. package/packages/core/dist/telegram-channel.js.map +1 -0
  111. package/packages/core/dist/thread-id.d.ts +1 -0
  112. package/packages/core/dist/thread-id.js +12 -0
  113. package/packages/core/dist/thread-id.js.map +1 -0
  114. package/packages/core/dist/whatsapp-channel.d.ts +26 -0
  115. package/packages/core/dist/whatsapp-channel.js +36 -0
  116. package/packages/core/dist/whatsapp-channel.js.map +1 -0
  117. package/packages/core/dist/workspace-paths.d.ts +5 -0
  118. package/packages/core/dist/workspace-paths.js +77 -0
  119. package/packages/core/dist/workspace-paths.js.map +1 -0
  120. package/packages/core/dist/workspace-policy.d.ts +30 -0
  121. package/packages/core/dist/workspace-policy.js +104 -0
  122. package/packages/core/dist/workspace-policy.js.map +1 -0
  123. package/packages/core/package.json +126 -0
  124. package/scripts/cli.mjs +537 -0
  125. package/scripts/configure.mjs +254 -0
  126. package/scripts/ensure-shared-build.mjs +96 -0
  127. package/scripts/run-node-tests.mjs +52 -0
  128. package/scripts/supervisor.mjs +332 -0
@@ -0,0 +1,752 @@
1
+ // @ts-nocheck
2
+ import { randomBytes } from "node:crypto";
3
+ const MENU_TTL_MS = 15 * 60 * 1000;
4
+ const FLOW_TTL_MS = 10 * 60 * 1000;
5
+ const TELEGRAM_VERIFY_TIMEOUT_MS = 10_000;
6
+ const BOT_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
7
+ const TELEGRAM_TOKEN_PATTERN = /^\d{5,}:[A-Za-z0-9_-]{20,}$/;
8
+ const menuSessions = new Map();
9
+ const createFlows = new Map();
10
+ const POLICY_PROFILES = {
11
+ safe: {
12
+ id: "safe",
13
+ label: "Safe",
14
+ hint: "read-only + approval prompts",
15
+ sandboxMode: "read-only",
16
+ approvalPolicy: "on-request",
17
+ },
18
+ standard: {
19
+ id: "standard",
20
+ label: "Standard",
21
+ hint: "workspace write + approval prompts",
22
+ sandboxMode: "workspace-write",
23
+ approvalPolicy: "on-request",
24
+ },
25
+ semi_auto: {
26
+ id: "semi_auto",
27
+ label: "Semi Auto",
28
+ hint: "workspace write + ask on failures",
29
+ sandboxMode: "workspace-write",
30
+ approvalPolicy: "on-failure",
31
+ },
32
+ full_auto: {
33
+ id: "full_auto",
34
+ label: "Full",
35
+ hint: "no approval prompts",
36
+ sandboxMode: "danger-full-access",
37
+ approvalPolicy: "never",
38
+ },
39
+ };
40
+ export async function maybeHandleHubOpsCommand({ ctx, runtime, channelId }) {
41
+ const text = String(ctx.message?.text ?? "").trim();
42
+ const command = extractCommand(text);
43
+ if (!command) {
44
+ return false;
45
+ }
46
+ cleanupState();
47
+ const chatId = getChatId(ctx);
48
+ const flowKey = buildFlowKey(runtime?.runtimeId, channelId, chatId);
49
+ if (command === "/start" || command === "/help") {
50
+ await ctx.reply(buildHelpText(runtime?.runtimeName));
51
+ return true;
52
+ }
53
+ if (command === "/health") {
54
+ try {
55
+ const health = await apiGet("/api/health");
56
+ await ctx.reply([
57
+ "Engine health:",
58
+ `ok: ${Boolean(health?.ok)}`,
59
+ `service: ${String(health?.service ?? "-")}`,
60
+ `botCount: ${Number(health?.botCount ?? 0)}`,
61
+ `webPort: ${String(health?.webPort ?? "-")}`,
62
+ ].join("\n"));
63
+ }
64
+ catch (error) {
65
+ await ctx.reply(`Health failed: ${sanitizeError(error)}`);
66
+ }
67
+ return true;
68
+ }
69
+ if (command === "/bots") {
70
+ try {
71
+ await renderBotsMenu(ctx, { editMessage: false });
72
+ }
73
+ catch (error) {
74
+ await ctx.reply(`Bots list failed: ${sanitizeError(error)}`);
75
+ }
76
+ return true;
77
+ }
78
+ if (command === "/create_agent") {
79
+ const existing = createFlows.get(flowKey);
80
+ if (existing) {
81
+ await ctx.reply("A create flow is already active. Send /cancel to abort it first.");
82
+ return true;
83
+ }
84
+ createFlows.set(flowKey, {
85
+ createdAt: Date.now(),
86
+ step: "token",
87
+ token: null,
88
+ tokenInfo: null,
89
+ botId: null,
90
+ });
91
+ await ctx.reply([
92
+ "Create agent wizard started.",
93
+ "Step 1: send Telegram bot token.",
94
+ "Format: 123456789:ABC...",
95
+ "Use /cancel to stop.",
96
+ ].join("\n"));
97
+ return true;
98
+ }
99
+ if (command === "/cancel") {
100
+ const deleted = createFlows.delete(flowKey);
101
+ await ctx.reply(deleted ? "Current operation canceled." : "No active operation.");
102
+ return true;
103
+ }
104
+ return false;
105
+ }
106
+ export async function maybeHandleHubOpsFollowUp({ ctx, runtime, channelId }) {
107
+ cleanupState();
108
+ const text = String(ctx.message?.text ?? "").trim();
109
+ if (!text || text.startsWith("/")) {
110
+ return false;
111
+ }
112
+ const chatId = getChatId(ctx);
113
+ const flowKey = buildFlowKey(runtime?.runtimeId, channelId, chatId);
114
+ const flow = createFlows.get(flowKey);
115
+ if (!flow) {
116
+ return false;
117
+ }
118
+ if (flow.step === "token") {
119
+ if (!TELEGRAM_TOKEN_PATTERN.test(text)) {
120
+ await ctx.reply("Token format looks invalid. Send a valid Telegram token or /cancel.");
121
+ return true;
122
+ }
123
+ const verification = await verifyTelegramToken(text);
124
+ if (!verification.ok) {
125
+ await ctx.reply(`Token verification failed:\n${verification.error}\nRetry or /cancel.`);
126
+ return true;
127
+ }
128
+ flow.token = text;
129
+ flow.tokenInfo = verification;
130
+ flow.step = "bot_id";
131
+ createFlows.set(flowKey, flow);
132
+ const suggestedBotId = suggestBotIdFromUsername(verification.username);
133
+ const suggestionText = suggestedBotId
134
+ ? `Suggested id: ${suggestedBotId}`
135
+ : "Suggested id: (none)";
136
+ await ctx.reply([
137
+ `Token valid for @${verification.username || "unknown"}.`,
138
+ suggestionText,
139
+ "Step 2: send agent id (letters/numbers/_/-).",
140
+ "You can also send: default",
141
+ ].join("\n"));
142
+ return true;
143
+ }
144
+ if (flow.step === "bot_id") {
145
+ const suggestedBotId = suggestBotIdFromUsername(flow.tokenInfo?.username);
146
+ const candidate = normalizeBotIdInput(text, suggestedBotId);
147
+ if (!candidate || !BOT_ID_PATTERN.test(candidate)) {
148
+ await ctx.reply("Invalid agent id. Use letters, numbers, underscore or dash.");
149
+ return true;
150
+ }
151
+ flow.botId = candidate;
152
+ flow.step = "confirm";
153
+ createFlows.set(flowKey, flow);
154
+ await ctx.reply([
155
+ "Step 3 confirm:",
156
+ `agentId: ${flow.botId}`,
157
+ `telegram: @${flow.tokenInfo?.username || "unknown"}`,
158
+ "Default policy after create: Full (no approval prompts).",
159
+ "Agent actions start from its own workspace folder.",
160
+ "Reply YES to create, or NO to cancel.",
161
+ ].join("\n"));
162
+ return true;
163
+ }
164
+ if (flow.step === "confirm") {
165
+ const decision = text.toLowerCase();
166
+ if (decision === "no" || decision === "n" || decision === "cancel") {
167
+ createFlows.delete(flowKey);
168
+ await ctx.reply("Create canceled.");
169
+ return true;
170
+ }
171
+ if (!(decision === "yes" || decision === "y" || decision === "ok")) {
172
+ await ctx.reply("Reply YES to create, or NO to cancel.");
173
+ return true;
174
+ }
175
+ try {
176
+ const result = await apiPost("/api/bots/create", {
177
+ agent: buildTelegramAgentDefinition({
178
+ botId: flow.botId,
179
+ token: flow.token,
180
+ }),
181
+ startIfEnabled: true,
182
+ });
183
+ createFlows.delete(flowKey);
184
+ await ctx.reply([
185
+ `Agent created: ${String(result?.bot?.id ?? flow.botId)}`,
186
+ "Use /bots to manage policy, reset context, or delete.",
187
+ ].join("\n"));
188
+ return true;
189
+ }
190
+ catch (error) {
191
+ await ctx.reply(`Create failed:\n${sanitizeError(error)}\nRetry or /cancel.`);
192
+ return true;
193
+ }
194
+ }
195
+ createFlows.delete(flowKey);
196
+ await ctx.reply("Flow reset. Use /create_agent to start again.");
197
+ return true;
198
+ }
199
+ export async function maybeHandleHubOpsCallback({ ctx }) {
200
+ const rawData = String(ctx.callbackQuery?.data ?? "").trim();
201
+ if (!rawData.startsWith("hub:")) {
202
+ return false;
203
+ }
204
+ cleanupState();
205
+ const action = parseMenuAction(rawData);
206
+ if (!action) {
207
+ await answerCallbackQuerySafe(ctx);
208
+ return true;
209
+ }
210
+ try {
211
+ if (action.type === "create") {
212
+ await answerCallbackQuerySafe(ctx, "Use /create_agent in this chat.");
213
+ await ctx.reply("Use /create_agent to create a new Telegram agent.");
214
+ return true;
215
+ }
216
+ if (action.type === "refresh") {
217
+ await renderBotsMenu(ctx, { editMessage: true });
218
+ await answerCallbackQuerySafe(ctx, "Updated");
219
+ return true;
220
+ }
221
+ if (action.type === "back") {
222
+ await renderBotsMenu(ctx, { editMessage: true });
223
+ await answerCallbackQuerySafe(ctx);
224
+ return true;
225
+ }
226
+ const session = getMenuSession(action.sessionId, ctx);
227
+ if (!session) {
228
+ await renderBotsMenu(ctx, { editMessage: true });
229
+ await answerCallbackQuerySafe(ctx, "Menu expired. Refreshed.");
230
+ return true;
231
+ }
232
+ const botId = getBotIdFromSession(session, action.index);
233
+ if (!botId) {
234
+ await renderBotsMenu(ctx, { editMessage: true });
235
+ await answerCallbackQuerySafe(ctx, "Agent not found. Refreshed.");
236
+ return true;
237
+ }
238
+ if (action.type === "open") {
239
+ await renderBotActions(ctx, {
240
+ sessionId: action.sessionId,
241
+ index: action.index,
242
+ });
243
+ await answerCallbackQuerySafe(ctx);
244
+ return true;
245
+ }
246
+ if (action.type === "policy") {
247
+ const profile = POLICY_PROFILES[action.profileId];
248
+ if (!profile) {
249
+ await answerCallbackQuerySafe(ctx, "Invalid profile.");
250
+ return true;
251
+ }
252
+ await apiPost(`/api/bots/${encodeURIComponent(botId)}/policy`, {
253
+ sandboxMode: profile.sandboxMode,
254
+ approvalPolicy: profile.approvalPolicy,
255
+ });
256
+ await renderBotActions(ctx, {
257
+ sessionId: action.sessionId,
258
+ index: action.index,
259
+ notice: `Policy updated: ${profile.label} (${profile.hint})`,
260
+ });
261
+ await answerCallbackQuerySafe(ctx, "Policy updated");
262
+ return true;
263
+ }
264
+ if (action.type === "reset_ask") {
265
+ await renderResetConfirm(ctx, {
266
+ sessionId: action.sessionId,
267
+ index: action.index,
268
+ botId,
269
+ });
270
+ await answerCallbackQuerySafe(ctx);
271
+ return true;
272
+ }
273
+ if (action.type === "reset_confirm") {
274
+ await apiPost(`/api/bots/${encodeURIComponent(botId)}/reset`, {});
275
+ await renderBotActions(ctx, {
276
+ sessionId: action.sessionId,
277
+ index: action.index,
278
+ notice: "Context reset completed.",
279
+ });
280
+ await answerCallbackQuerySafe(ctx, "Context reset");
281
+ return true;
282
+ }
283
+ if (action.type === "delete_ask") {
284
+ await renderDeleteConfirm(ctx, {
285
+ sessionId: action.sessionId,
286
+ index: action.index,
287
+ botId,
288
+ });
289
+ await answerCallbackQuerySafe(ctx);
290
+ return true;
291
+ }
292
+ if (action.type === "delete_confirm") {
293
+ await apiPost(`/api/bots/${encodeURIComponent(botId)}/delete`, { deleteMode: "soft" });
294
+ await renderBotsMenu(ctx, {
295
+ editMessage: true,
296
+ notice: `Agent deleted: ${botId}`,
297
+ });
298
+ await answerCallbackQuerySafe(ctx, "Agent deleted");
299
+ return true;
300
+ }
301
+ await answerCallbackQuerySafe(ctx);
302
+ return true;
303
+ }
304
+ catch (error) {
305
+ await answerCallbackQuerySafe(ctx, "Action failed");
306
+ await editMessageOrReply(ctx, `Action failed:\n${sanitizeError(error)}`, {
307
+ reply_markup: {
308
+ inline_keyboard: [[{ text: "Back to bots", callback_data: "hub:back" }]],
309
+ },
310
+ });
311
+ return true;
312
+ }
313
+ }
314
+ function buildHelpText(runtimeName) {
315
+ return [
316
+ `${String(runtimeName ?? "Copilot Hub")}`,
317
+ "",
318
+ "Commands:",
319
+ "/help",
320
+ "/health",
321
+ "/bots",
322
+ "/create_agent",
323
+ "/cancel",
324
+ "",
325
+ "Policy guide in /bots:",
326
+ "Safe: read-only + approval prompts",
327
+ "Standard: workspace write + approval prompts",
328
+ "Semi Auto: workspace write + ask on failures",
329
+ "Full: no approval prompts",
330
+ "All agent actions start from that agent workspace.",
331
+ "",
332
+ "For development tasks, send a normal message to the assistant.",
333
+ ].join("\n");
334
+ }
335
+ function buildFlowKey(runtimeId, channelId, chatId) {
336
+ return `${String(runtimeId ?? "hub")}::${String(channelId ?? "telegram")}::${String(chatId ?? "")}`;
337
+ }
338
+ function cleanupState() {
339
+ const now = Date.now();
340
+ for (const [sessionId, session] of menuSessions.entries()) {
341
+ const createdAt = Number(session?.createdAt ?? 0);
342
+ if (!Number.isFinite(createdAt) || now - createdAt > MENU_TTL_MS) {
343
+ menuSessions.delete(sessionId);
344
+ }
345
+ }
346
+ for (const [key, flow] of createFlows.entries()) {
347
+ const createdAt = Number(flow?.createdAt ?? 0);
348
+ if (!Number.isFinite(createdAt) || now - createdAt > FLOW_TTL_MS) {
349
+ createFlows.delete(key);
350
+ }
351
+ }
352
+ }
353
+ async function renderBotsMenu(ctx, { editMessage = false, notice = "" } = {}) {
354
+ const bots = await fetchBots();
355
+ const chatId = getChatId(ctx);
356
+ const sessionId = createMenuSession(chatId, bots);
357
+ const lines = [];
358
+ if (notice) {
359
+ lines.push(notice, "");
360
+ }
361
+ lines.push("Agents:");
362
+ if (bots.length === 0) {
363
+ lines.push("No bots registered.");
364
+ }
365
+ else {
366
+ for (const botState of bots) {
367
+ const status = botState.running ? "ON" : "OFF";
368
+ lines.push(`- ${botState.id} (${status})`);
369
+ }
370
+ lines.push("", "Tap an agent below.");
371
+ }
372
+ const keyboard = buildBotsMenuKeyboard(sessionId, bots);
373
+ if (editMessage) {
374
+ await editMessageOrReply(ctx, lines.join("\n"), {
375
+ reply_markup: {
376
+ inline_keyboard: keyboard,
377
+ },
378
+ });
379
+ return;
380
+ }
381
+ await ctx.reply(lines.join("\n"), {
382
+ reply_markup: {
383
+ inline_keyboard: keyboard,
384
+ },
385
+ });
386
+ }
387
+ async function renderBotActions(ctx, { sessionId, index, notice = "" }) {
388
+ const session = getMenuSession(sessionId, ctx);
389
+ const botId = getBotIdFromSession(session, index);
390
+ if (!botId) {
391
+ await renderBotsMenu(ctx, { editMessage: true, notice: "Agent not found. Refreshed." });
392
+ return;
393
+ }
394
+ const botState = await fetchBotById(botId);
395
+ if (!botState) {
396
+ await renderBotsMenu(ctx, { editMessage: true, notice: `Agent '${botId}' not found.` });
397
+ return;
398
+ }
399
+ const providerOptions = botState?.provider?.options && typeof botState.provider.options === "object"
400
+ ? botState.provider.options
401
+ : {};
402
+ const lines = [];
403
+ if (notice) {
404
+ lines.push(notice, "");
405
+ }
406
+ lines.push(`Agent: ${botState.id}`, `running: ${botState.running ? "yes" : "no"}`, `telegram: ${botState.telegramRunning ? "yes" : "no"}`, `sandboxMode: ${String(providerOptions.sandboxMode ?? "-")}`, `approvalPolicy: ${String(providerOptions.approvalPolicy ?? "-")}`, "", "Policy quick guide:", "Safe = read-only + approval prompts", "Standard = workspace write + approval prompts", "Semi Auto = workspace write + ask on failures", "Full = no approval prompts", "All actions start from this agent workspace.", "", "Choose an action:");
407
+ await editMessageOrReply(ctx, lines.join("\n"), {
408
+ reply_markup: {
409
+ inline_keyboard: buildBotActionsKeyboard(sessionId, index),
410
+ },
411
+ });
412
+ }
413
+ async function renderResetConfirm(ctx, { sessionId, index, botId }) {
414
+ await editMessageOrReply(ctx, [`Reset context for '${botId}'?`, "This clears the current web thread context."].join("\n"), {
415
+ reply_markup: {
416
+ inline_keyboard: [
417
+ [
418
+ { text: "Confirm reset", callback_data: `hub:rc:${sessionId}:${index}` },
419
+ { text: "Cancel", callback_data: `hub:o:${sessionId}:${index}` },
420
+ ],
421
+ [{ text: "Back to bots", callback_data: "hub:back" }],
422
+ ],
423
+ },
424
+ });
425
+ }
426
+ async function renderDeleteConfirm(ctx, { sessionId, index, botId }) {
427
+ await editMessageOrReply(ctx, [`Delete agent '${botId}'?`, "This stops and removes the agent from runtime."].join("\n"), {
428
+ reply_markup: {
429
+ inline_keyboard: [
430
+ [
431
+ { text: "Confirm delete", callback_data: `hub:dc:${sessionId}:${index}` },
432
+ { text: "Cancel", callback_data: `hub:o:${sessionId}:${index}` },
433
+ ],
434
+ [{ text: "Back to bots", callback_data: "hub:back" }],
435
+ ],
436
+ },
437
+ });
438
+ }
439
+ function buildBotsMenuKeyboard(sessionId, bots) {
440
+ const rows = [];
441
+ for (let index = 0; index < bots.length; index += 1) {
442
+ const botState = bots[index];
443
+ const status = botState.running ? "ON" : "OFF";
444
+ rows.push([
445
+ { text: `${botState.id} (${status})`, callback_data: `hub:o:${sessionId}:${index}` },
446
+ ]);
447
+ }
448
+ rows.push([{ text: "Refresh", callback_data: `hub:r:${sessionId}` }]);
449
+ rows.push([{ text: "Create agent", callback_data: "hub:create" }]);
450
+ return rows;
451
+ }
452
+ function buildBotActionsKeyboard(sessionId, index) {
453
+ return [
454
+ [
455
+ { text: "Safe (read-only)", callback_data: `hub:p:${sessionId}:${index}:safe` },
456
+ { text: "Standard (ask)", callback_data: `hub:p:${sessionId}:${index}:standard` },
457
+ ],
458
+ [
459
+ { text: "Semi (fail ask)", callback_data: `hub:p:${sessionId}:${index}:semi_auto` },
460
+ { text: "Full (no prompts)", callback_data: `hub:p:${sessionId}:${index}:full_auto` },
461
+ ],
462
+ [{ text: "Reset Context", callback_data: `hub:ra:${sessionId}:${index}` }],
463
+ [{ text: "Delete Agent", callback_data: `hub:da:${sessionId}:${index}` }],
464
+ [{ text: "Back to bots", callback_data: "hub:back" }],
465
+ ];
466
+ }
467
+ function createMenuSession(chatId, bots) {
468
+ let sessionId = "";
469
+ for (let attempt = 0; attempt < 5; attempt += 1) {
470
+ const candidate = randomBytes(4).toString("hex");
471
+ if (!menuSessions.has(candidate)) {
472
+ sessionId = candidate;
473
+ break;
474
+ }
475
+ }
476
+ if (!sessionId) {
477
+ sessionId = `${Date.now().toString(36)}${Math.floor(Math.random() * 9999).toString(36)}`;
478
+ }
479
+ menuSessions.set(sessionId, {
480
+ chatId,
481
+ createdAt: Date.now(),
482
+ botIds: bots.map((entry) => String(entry?.id ?? "").trim()).filter(Boolean),
483
+ });
484
+ return sessionId;
485
+ }
486
+ function getMenuSession(sessionId, ctx) {
487
+ const session = menuSessions.get(sessionId);
488
+ if (!session) {
489
+ return null;
490
+ }
491
+ const chatId = getChatId(ctx);
492
+ if (!chatId || session.chatId !== chatId) {
493
+ return null;
494
+ }
495
+ return session;
496
+ }
497
+ function getBotIdFromSession(session, index) {
498
+ if (!session) {
499
+ return null;
500
+ }
501
+ if (!Number.isInteger(index) || index < 0 || index >= session.botIds.length) {
502
+ return null;
503
+ }
504
+ const botId = String(session.botIds[index] ?? "").trim();
505
+ return botId || null;
506
+ }
507
+ function parseMenuAction(rawData) {
508
+ const data = String(rawData ?? "").trim();
509
+ if (!data || !data.startsWith("hub:")) {
510
+ return null;
511
+ }
512
+ if (data === "hub:back") {
513
+ return { type: "back" };
514
+ }
515
+ if (data === "hub:create") {
516
+ return { type: "create" };
517
+ }
518
+ const parts = data.split(":");
519
+ if (parts.length < 3) {
520
+ return null;
521
+ }
522
+ const kind = parts[1];
523
+ if (kind === "r" && parts.length === 3) {
524
+ return {
525
+ type: "refresh",
526
+ sessionId: parts[2],
527
+ };
528
+ }
529
+ if ((kind === "o" || kind === "ra" || kind === "rc" || kind === "da" || kind === "dc") &&
530
+ parts.length === 4) {
531
+ const index = parseMenuIndex(parts[3]);
532
+ if (index === null) {
533
+ return null;
534
+ }
535
+ const mapping = {
536
+ o: "open",
537
+ ra: "reset_ask",
538
+ rc: "reset_confirm",
539
+ da: "delete_ask",
540
+ dc: "delete_confirm",
541
+ };
542
+ return {
543
+ type: mapping[kind],
544
+ sessionId: parts[2],
545
+ index,
546
+ };
547
+ }
548
+ if (kind === "p" && parts.length === 5) {
549
+ const index = parseMenuIndex(parts[3]);
550
+ const profileId = String(parts[4] ?? "")
551
+ .trim()
552
+ .toLowerCase();
553
+ if (index === null || !profileId) {
554
+ return null;
555
+ }
556
+ return {
557
+ type: "policy",
558
+ sessionId: parts[2],
559
+ index,
560
+ profileId,
561
+ };
562
+ }
563
+ return null;
564
+ }
565
+ function parseMenuIndex(value) {
566
+ const index = Number.parseInt(String(value ?? ""), 10);
567
+ if (!Number.isFinite(index) || index < 0 || index > 9999) {
568
+ return null;
569
+ }
570
+ return index;
571
+ }
572
+ function extractCommand(text) {
573
+ const token = String(text ?? "")
574
+ .trim()
575
+ .split(/\s+/)[0] ?? "";
576
+ if (!token.startsWith("/")) {
577
+ return "";
578
+ }
579
+ return token.split("@")[0].toLowerCase();
580
+ }
581
+ function normalizeBotIdInput(value, suggested) {
582
+ const raw = String(value ?? "").trim();
583
+ if (!raw) {
584
+ return null;
585
+ }
586
+ const normalized = raw.toLowerCase();
587
+ if ((normalized === "default" || normalized === "auto") && suggested) {
588
+ return suggested;
589
+ }
590
+ if (!BOT_ID_PATTERN.test(raw)) {
591
+ return null;
592
+ }
593
+ return raw;
594
+ }
595
+ function suggestBotIdFromUsername(username) {
596
+ const raw = String(username ?? "")
597
+ .trim()
598
+ .toLowerCase();
599
+ if (!raw) {
600
+ return null;
601
+ }
602
+ const candidate = raw
603
+ .replace(/[^a-z0-9_-]/g, "_")
604
+ .replace(/^_+|_+$/g, "")
605
+ .slice(0, 64);
606
+ if (!candidate || !BOT_ID_PATTERN.test(candidate)) {
607
+ return null;
608
+ }
609
+ return candidate;
610
+ }
611
+ function buildTelegramAgentDefinition({ botId, token }) {
612
+ const safeId = String(botId ?? "").trim();
613
+ return {
614
+ id: safeId,
615
+ name: safeId,
616
+ enabled: true,
617
+ autoStart: true,
618
+ threadMode: "single",
619
+ sharedThreadId: `shared-${safeId}`,
620
+ provider: {
621
+ kind: "codex",
622
+ options: {
623
+ sandboxMode: "danger-full-access",
624
+ approvalPolicy: "never",
625
+ },
626
+ },
627
+ channels: [
628
+ {
629
+ kind: "telegram",
630
+ id: `telegram_${safeId}`,
631
+ token: String(token ?? "").trim(),
632
+ },
633
+ ],
634
+ capabilities: [],
635
+ };
636
+ }
637
+ async function fetchBots() {
638
+ const payload = await apiGet("/api/bots");
639
+ const bots = Array.isArray(payload?.bots) ? payload.bots : [];
640
+ return bots
641
+ .filter((entry) => String(entry?.id ?? "").trim() !== "")
642
+ .sort((a, b) => String(a.id ?? "").localeCompare(String(b.id ?? "")));
643
+ }
644
+ async function fetchBotById(botId) {
645
+ const bots = await fetchBots();
646
+ return bots.find((entry) => String(entry?.id ?? "").trim() === botId) ?? null;
647
+ }
648
+ async function editMessageOrReply(ctx, text, options = {}) {
649
+ try {
650
+ await ctx.editMessageText(text, options);
651
+ }
652
+ catch (error) {
653
+ const message = sanitizeError(error).toLowerCase();
654
+ if (message.includes("message is not modified")) {
655
+ return;
656
+ }
657
+ await ctx.reply(text, options);
658
+ }
659
+ }
660
+ function getChatId(ctx) {
661
+ return String(ctx.chat?.id ?? ctx.callbackQuery?.message?.chat?.id ?? "").trim();
662
+ }
663
+ async function answerCallbackQuerySafe(ctx, text = "") {
664
+ try {
665
+ if (text) {
666
+ await ctx.answerCallbackQuery({ text });
667
+ return;
668
+ }
669
+ await ctx.answerCallbackQuery();
670
+ }
671
+ catch {
672
+ // ignore
673
+ }
674
+ }
675
+ async function apiGet(endpoint) {
676
+ const response = await fetch(`${getEngineBaseUrl()}${endpoint}`, {
677
+ method: "GET",
678
+ headers: {
679
+ Accept: "application/json",
680
+ },
681
+ });
682
+ return parseJsonResponse(response);
683
+ }
684
+ async function apiPost(endpoint, body) {
685
+ const response = await fetch(`${getEngineBaseUrl()}${endpoint}`, {
686
+ method: "POST",
687
+ headers: {
688
+ "Content-Type": "application/json",
689
+ Accept: "application/json",
690
+ },
691
+ body: JSON.stringify(body ?? {}),
692
+ });
693
+ return parseJsonResponse(response);
694
+ }
695
+ function getEngineBaseUrl() {
696
+ const value = String(process.env.HUB_ENGINE_BASE_URL ?? "http://127.0.0.1:8787").trim();
697
+ return value.replace(/\/+$/, "") || "http://127.0.0.1:8787";
698
+ }
699
+ async function parseJsonResponse(response) {
700
+ const payload = await response.json().catch(() => null);
701
+ if (!response.ok) {
702
+ const detail = payload?.error ? `: ${payload.error}` : "";
703
+ throw new Error(`HTTP ${response.status}${detail}`);
704
+ }
705
+ return payload;
706
+ }
707
+ async function verifyTelegramToken(token) {
708
+ const controller = new AbortController();
709
+ const timer = setTimeout(() => controller.abort(), TELEGRAM_VERIFY_TIMEOUT_MS);
710
+ try {
711
+ const response = await fetch(`https://api.telegram.org/bot${token}/getMe`, {
712
+ method: "GET",
713
+ signal: controller.signal,
714
+ headers: {
715
+ Accept: "application/json",
716
+ },
717
+ });
718
+ const payload = await response.json().catch(() => null);
719
+ if (!response.ok || payload?.ok !== true || !payload?.result) {
720
+ const reason = String(payload?.description ?? `HTTP ${response.status}`).trim();
721
+ return {
722
+ ok: false,
723
+ error: reason || "Telegram getMe failed.",
724
+ };
725
+ }
726
+ return {
727
+ ok: true,
728
+ id: payload.result.id,
729
+ username: String(payload.result.username ?? "").trim() || null,
730
+ };
731
+ }
732
+ catch (error) {
733
+ const reason = sanitizeError(error);
734
+ if (reason.toLowerCase().includes("aborted")) {
735
+ return {
736
+ ok: false,
737
+ error: "Telegram verification timed out.",
738
+ };
739
+ }
740
+ return {
741
+ ok: false,
742
+ error: reason,
743
+ };
744
+ }
745
+ finally {
746
+ clearTimeout(timer);
747
+ }
748
+ }
749
+ function sanitizeError(error) {
750
+ const raw = error instanceof Error ? error.message : String(error);
751
+ return raw.split(/\r?\n/).slice(0, 6).join("\n");
752
+ }