@chrysb/alphaclaw 0.3.3 → 0.3.4

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 (31) hide show
  1. package/bin/alphaclaw.js +18 -0
  2. package/lib/plugin/usage-tracker/index.js +308 -0
  3. package/lib/plugin/usage-tracker/openclaw.plugin.json +8 -0
  4. package/lib/public/css/explorer.css +51 -1
  5. package/lib/public/css/shell.css +3 -1
  6. package/lib/public/css/theme.css +35 -0
  7. package/lib/public/js/app.js +73 -24
  8. package/lib/public/js/components/file-tree.js +231 -28
  9. package/lib/public/js/components/file-viewer.js +193 -20
  10. package/lib/public/js/components/segmented-control.js +33 -0
  11. package/lib/public/js/components/sidebar.js +14 -32
  12. package/lib/public/js/components/telegram-workspace/index.js +353 -0
  13. package/lib/public/js/components/telegram-workspace/manage.js +397 -0
  14. package/lib/public/js/components/telegram-workspace/onboarding.js +616 -0
  15. package/lib/public/js/components/usage-tab.js +528 -0
  16. package/lib/public/js/components/watchdog-tab.js +1 -1
  17. package/lib/public/js/lib/api.js +25 -1
  18. package/lib/public/js/lib/telegram-api.js +78 -0
  19. package/lib/public/js/lib/ui-settings.js +38 -0
  20. package/lib/public/setup.html +34 -30
  21. package/lib/server/alphaclaw-version.js +3 -3
  22. package/lib/server/constants.js +1 -0
  23. package/lib/server/onboarding/openclaw.js +15 -0
  24. package/lib/server/routes/auth.js +5 -1
  25. package/lib/server/routes/telegram.js +185 -60
  26. package/lib/server/routes/usage.js +133 -0
  27. package/lib/server/usage-db.js +570 -0
  28. package/lib/server.js +21 -1
  29. package/lib/setup/core-prompts/AGENTS.md +0 -101
  30. package/package.json +1 -1
  31. package/lib/public/js/components/telegram-workspace.js +0 -1365
@@ -1,33 +1,37 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>alphaclaw</title>
7
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
8
- <link rel="icon" type="image/svg+xml" href="./img/logo.svg">
9
- <link rel="stylesheet" href="./css/theme.css">
10
- <link rel="stylesheet" href="./css/shell.css">
11
- <link rel="stylesheet" href="./css/explorer.css">
12
- <script src="https://cdn.tailwindcss.com"></script>
13
- <script>
14
- tailwind.config = {
15
- theme: {
16
- extend: {
17
- colors: {
18
- surface: 'var(--bg-sidebar)',
19
- border: 'var(--border)',
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>alphaclaw</title>
7
+ <link
8
+ href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap"
9
+ rel="stylesheet"
10
+ />
11
+ <link rel="icon" type="image/svg+xml" href="./img/logo.svg" />
12
+ <link rel="stylesheet" href="./css/theme.css" />
13
+ <link rel="stylesheet" href="./css/shell.css" />
14
+ <link rel="stylesheet" href="./css/explorer.css" />
15
+ <script src="https://cdn.tailwindcss.com"></script>
16
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
17
+ <script>
18
+ tailwind.config = {
19
+ theme: {
20
+ extend: {
21
+ colors: {
22
+ surface: "var(--bg-sidebar)",
23
+ border: "var(--border)",
24
+ },
25
+ fontFamily: {
26
+ mono: ["'JetBrains Mono'", "monospace"],
27
+ },
20
28
  },
21
- fontFamily: {
22
- mono: ["'JetBrains Mono'", 'monospace'],
23
- }
24
- }
25
- }
26
- }
27
- </script>
28
- </head>
29
- <body>
30
- <div id="app"></div>
31
- <script type="module" src="./js/app.js"></script>
32
- </body>
29
+ },
30
+ };
31
+ </script>
32
+ </head>
33
+ <body>
34
+ <div id="app"></div>
35
+ <script type="module" src="./js/app.js"></script>
36
+ </body>
33
37
  </html>
@@ -1,4 +1,4 @@
1
- const { exec } = require("child_process");
1
+ const childProcess = require("child_process");
2
2
  const fs = require("fs");
3
3
  const path = require("path");
4
4
  const https = require("https");
@@ -140,8 +140,8 @@ const createAlphaclawVersionService = () => {
140
140
  console.log(
141
141
  `[alphaclaw] Running: npm install @chrysb/alphaclaw@latest (cwd: ${installDir})`,
142
142
  );
143
- exec(
144
- "npm install @chrysb/alphaclaw@latest --omit=dev --no-save --package-lock=false --prefer-online",
143
+ childProcess.exec(
144
+ "npm install @chrysb/alphaclaw@latest --omit=dev --no-save --save=false --package-lock=false --prefer-online",
145
145
  {
146
146
  cwd: installDir,
147
147
  env: {
@@ -293,6 +293,7 @@ const SETUP_API_PREFIXES = [
293
293
  "/api/telegram",
294
294
  "/api/webhooks",
295
295
  "/api/watchdog",
296
+ "/api/usage",
296
297
  ];
297
298
 
298
299
  module.exports = {
@@ -1,5 +1,14 @@
1
+ const path = require("path");
1
2
  const { buildSecretReplacements } = require("../helpers");
2
3
 
4
+ const kUsageTrackerPluginPath = path.resolve(
5
+ __dirname,
6
+ "..",
7
+ "..",
8
+ "plugin",
9
+ "usage-tracker",
10
+ );
11
+
3
12
  const buildOnboardArgs = ({ varMap, selectedProvider, hasCodexOauth, workspaceDir }) => {
4
13
  const onboardArgs = [
5
14
  "--non-interactive",
@@ -116,6 +125,8 @@ const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
116
125
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
117
126
  if (!cfg.channels) cfg.channels = {};
118
127
  if (!cfg.plugins) cfg.plugins = {};
128
+ if (!cfg.plugins.load) cfg.plugins.load = {};
129
+ if (!Array.isArray(cfg.plugins.load.paths)) cfg.plugins.load.paths = [];
119
130
  if (!cfg.plugins.entries) cfg.plugins.entries = {};
120
131
  if (!cfg.commands) cfg.commands = {};
121
132
  if (!cfg.hooks) cfg.hooks = {};
@@ -149,6 +160,10 @@ const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
149
160
  cfg.plugins.entries.discord = { enabled: true };
150
161
  console.log("[onboard] Discord configured");
151
162
  }
163
+ if (!cfg.plugins.load.paths.includes(kUsageTrackerPluginPath)) {
164
+ cfg.plugins.load.paths.push(kUsageTrackerPluginPath);
165
+ }
166
+ cfg.plugins.entries["usage-tracker"] = { enabled: true };
152
167
 
153
168
  let content = JSON.stringify(cfg, null, 2);
154
169
  const replacements = buildSecretReplacements(varMap, process.env);
@@ -48,7 +48,11 @@ const registerAuthRoutes = ({ app, loginThrottle }) => {
48
48
 
49
49
  const cookieParser = (req) => {
50
50
  const cookies = {};
51
- (req.headers.cookie || "").split(";").forEach((c) => {
51
+ const cookieHeader =
52
+ req && req.headers && typeof req.headers.cookie === "string"
53
+ ? req.headers.cookie
54
+ : "";
55
+ cookieHeader.split(";").forEach((c) => {
52
56
  const [k, ...v] = c.trim().split("=");
53
57
  if (k) cookies[k] = v.join("=");
54
58
  });
@@ -19,7 +19,11 @@ const resolveGroupId = (req) => {
19
19
  const rawGroupId = body.groupId ?? body.chatId;
20
20
  return rawGroupId == null ? "" : String(rawGroupId).trim();
21
21
  };
22
- const resolveAllowUserId = async ({ telegramApi, groupId, preferredUserId }) => {
22
+ const resolveAllowUserId = async ({
23
+ telegramApi,
24
+ groupId,
25
+ preferredUserId,
26
+ }) => {
23
27
  const normalizedPreferred = String(preferredUserId || "").trim();
24
28
  if (normalizedPreferred) return normalizedPreferred;
25
29
  const admins = await telegramApi.getChatAdministrators(groupId);
@@ -43,7 +47,71 @@ const isMissingTopicError = (errorMessage) => {
43
47
  ].some((token) => message.includes(token));
44
48
  };
45
49
 
46
- const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
50
+ const normalizeGitSyncMessagePart = (value) =>
51
+ String(value || "")
52
+ .replace(/[\r\n\t]+/g, " ")
53
+ .replace(/\s+/g, " ")
54
+ .trim();
55
+
56
+ const quoteShellArg = (value) => `'${String(value || "").replace(/'/g, `'\"'\"'`)}'`;
57
+
58
+ const buildTelegramGitSyncCommand = (action, target = "") => {
59
+ const safeAction = normalizeGitSyncMessagePart(action);
60
+ const safeTarget = normalizeGitSyncMessagePart(target);
61
+ const message = `telegram workspace: ${safeAction} ${safeTarget}`.trim();
62
+ return `alphaclaw git-sync -m ${quoteShellArg(message)}`;
63
+ };
64
+
65
+ const registerTelegramRoutes = ({
66
+ app,
67
+ telegramApi,
68
+ syncPromptFiles,
69
+ shellCmd,
70
+ }) => {
71
+ const repairGroupAllowFromIfMissing = async ({
72
+ cfg,
73
+ groupId,
74
+ requireMention = false,
75
+ }) => {
76
+ const telegramConfig = cfg?.channels?.telegram || {};
77
+ if (
78
+ Array.isArray(telegramConfig.groupAllowFrom) &&
79
+ telegramConfig.groupAllowFrom.length > 0
80
+ ) {
81
+ return { repaired: false, resolvedUserId: "", syncWarning: null };
82
+ }
83
+ const resolvedUserId = await resolveAllowUserId({
84
+ telegramApi,
85
+ groupId,
86
+ preferredUserId: "",
87
+ });
88
+ syncConfigForTelegram({
89
+ fs,
90
+ openclawDir: OPENCLAW_DIR,
91
+ topicRegistry,
92
+ groupId,
93
+ requireMention,
94
+ resolvedUserId,
95
+ });
96
+ const syncWarning = await runTelegramGitSync(
97
+ "repair-group-allow-from",
98
+ groupId,
99
+ );
100
+ return { repaired: true, resolvedUserId, syncWarning };
101
+ };
102
+
103
+ const runTelegramGitSync = async (action, target = "") => {
104
+ if (typeof shellCmd !== "function") return null;
105
+ try {
106
+ await shellCmd(buildTelegramGitSyncCommand(action, target), {
107
+ timeout: 30000,
108
+ });
109
+ return null;
110
+ } catch (err) {
111
+ return err?.message || "alphaclaw git-sync failed";
112
+ }
113
+ };
114
+
47
115
  // Verify bot token
48
116
  app.get("/api/telegram/bot", async (req, res) => {
49
117
  try {
@@ -57,7 +125,8 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
57
125
  // Verify group: checks bot membership, admin rights, topics enabled
58
126
  app.post("/api/telegram/groups/verify", async (req, res) => {
59
127
  const groupId = resolveGroupId(req);
60
- if (!groupId) return res.status(400).json({ ok: false, error: "groupId is required" });
128
+ if (!groupId)
129
+ return res.status(400).json({ ok: false, error: "groupId is required" });
61
130
 
62
131
  try {
63
132
  const chat = await telegramApi.getChat(groupId);
@@ -69,7 +138,8 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
69
138
  preferredUserId: "",
70
139
  });
71
140
 
72
- const isAdmin = member.status === "administrator" || member.status === "creator";
141
+ const isAdmin =
142
+ member.status === "administrator" || member.status === "creator";
73
143
  const isForum = !!chat.is_forum;
74
144
 
75
145
  res.json({
@@ -83,7 +153,7 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
83
153
  bot: {
84
154
  status: member.status,
85
155
  isAdmin,
86
- canManageTopics: isAdmin && (member.can_manage_topics !== false),
156
+ canManageTopics: isAdmin && member.can_manage_topics !== false,
87
157
  },
88
158
  suggestedUserId: suggestedUserId || null,
89
159
  });
@@ -107,9 +177,13 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
107
177
  const systemInstructions = String(
108
178
  body.systemInstructions ?? body.systemPrompt ?? "",
109
179
  ).trim();
110
- const iconColorValue = rawIconColor == null ? null : Number.parseInt(String(rawIconColor), 10);
111
- const iconColor = Number.isFinite(iconColorValue) ? iconColorValue : undefined;
112
- if (!name) return res.status(400).json({ ok: false, error: "name is required" });
180
+ const iconColorValue =
181
+ rawIconColor == null ? null : Number.parseInt(String(rawIconColor), 10);
182
+ const iconColor = Number.isFinite(iconColorValue)
183
+ ? iconColorValue
184
+ : undefined;
185
+ if (!name)
186
+ return res.status(400).json({ ok: false, error: "name is required" });
113
187
 
114
188
  try {
115
189
  const result = await telegramApi.createForumTopic(groupId, name, {
@@ -130,7 +204,16 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
130
204
  resolvedUserId: "",
131
205
  });
132
206
  syncPromptFiles();
133
- res.json({ ok: true, topic: { threadId, name: result.name, iconColor: result.icon_color } });
207
+ const syncWarning = await runTelegramGitSync("create-topic", result.name);
208
+ res.json({
209
+ ok: true,
210
+ topic: {
211
+ threadId,
212
+ name: result.name,
213
+ iconColor: result.icon_color,
214
+ },
215
+ syncWarning,
216
+ });
134
217
  } catch (e) {
135
218
  res.json({ ok: false, error: e.message });
136
219
  }
@@ -142,7 +225,9 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
142
225
  const body = req.body || {};
143
226
  const topics = Array.isArray(body.topics) ? body.topics : [];
144
227
  if (!Array.isArray(topics) || topics.length === 0) {
145
- return res.status(400).json({ ok: false, error: "topics array is required" });
228
+ return res
229
+ .status(400)
230
+ .json({ ok: false, error: "topics array is required" });
146
231
  }
147
232
 
148
233
  const results = [];
@@ -156,7 +241,9 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
156
241
  iconColor: t.iconColor || undefined,
157
242
  });
158
243
  const threadId = result.message_thread_id;
159
- const systemInstructions = String(t.systemInstructions ?? t.systemPrompt ?? "").trim();
244
+ const systemInstructions = String(
245
+ t.systemInstructions ?? t.systemPrompt ?? "",
246
+ ).trim();
160
247
  topicRegistry.addTopic(groupId, threadId, {
161
248
  name: result.name,
162
249
  iconColor: result.icon_color,
@@ -176,64 +263,80 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
176
263
  resolvedUserId: "",
177
264
  });
178
265
  syncPromptFiles();
179
- res.json({ ok: true, results });
266
+ const syncWarning = await runTelegramGitSync("bulk-create-topics", groupId);
267
+ res.json({ ok: true, results, syncWarning });
180
268
  });
181
269
 
182
270
  // Delete a topic
183
- app.delete("/api/telegram/groups/:groupId/topics/:topicId", async (req, res) => {
184
- const { groupId, topicId } = req.params;
185
- try {
186
- await telegramApi.deleteForumTopic(groupId, parseInt(topicId, 10));
187
- topicRegistry.removeTopic(groupId, topicId);
188
- syncConfigForTelegram({
189
- fs,
190
- openclawDir: OPENCLAW_DIR,
191
- topicRegistry,
192
- groupId,
193
- requireMention: false,
194
- resolvedUserId: "",
195
- });
196
- syncPromptFiles();
197
- res.json({ ok: true });
198
- } catch (e) {
199
- if (!isMissingTopicError(e?.message)) {
200
- return res.json({ ok: false, error: e.message });
271
+ app.delete(
272
+ "/api/telegram/groups/:groupId/topics/:topicId",
273
+ async (req, res) => {
274
+ const { groupId, topicId } = req.params;
275
+ try {
276
+ await telegramApi.deleteForumTopic(groupId, parseInt(topicId, 10));
277
+ topicRegistry.removeTopic(groupId, topicId);
278
+ syncConfigForTelegram({
279
+ fs,
280
+ openclawDir: OPENCLAW_DIR,
281
+ topicRegistry,
282
+ groupId,
283
+ requireMention: false,
284
+ resolvedUserId: "",
285
+ });
286
+ syncPromptFiles();
287
+ const syncWarning = await runTelegramGitSync("delete-topic", topicId);
288
+ res.json({ ok: true, syncWarning });
289
+ } catch (e) {
290
+ if (!isMissingTopicError(e?.message)) {
291
+ return res.json({ ok: false, error: e.message });
292
+ }
293
+ topicRegistry.removeTopic(groupId, topicId);
294
+ syncConfigForTelegram({
295
+ fs,
296
+ openclawDir: OPENCLAW_DIR,
297
+ topicRegistry,
298
+ groupId,
299
+ requireMention: false,
300
+ resolvedUserId: "",
301
+ });
302
+ syncPromptFiles();
303
+ const syncWarning = await runTelegramGitSync(
304
+ "delete-stale-topic",
305
+ topicId,
306
+ );
307
+ return res.json({
308
+ ok: true,
309
+ removedFromRegistryOnly: true,
310
+ warning:
311
+ "Topic no longer exists in Telegram; removed stale registry entry.",
312
+ syncWarning,
313
+ });
201
314
  }
202
- topicRegistry.removeTopic(groupId, topicId);
203
- syncConfigForTelegram({
204
- fs,
205
- openclawDir: OPENCLAW_DIR,
206
- topicRegistry,
207
- groupId,
208
- requireMention: false,
209
- resolvedUserId: "",
210
- });
211
- syncPromptFiles();
212
- return res.json({
213
- ok: true,
214
- removedFromRegistryOnly: true,
215
- warning: "Topic no longer exists in Telegram; removed stale registry entry.",
216
- });
217
- }
218
- });
315
+ },
316
+ );
219
317
 
220
318
  // Rename a topic
221
319
  app.put("/api/telegram/groups/:groupId/topics/:topicId", async (req, res) => {
222
320
  const { groupId, topicId } = req.params;
223
321
  const body = req.body || {};
224
322
  const name = String(body.name ?? "").trim();
225
- const hasSystemInstructions = Object.prototype.hasOwnProperty.call(body, "systemInstructions")
226
- || Object.prototype.hasOwnProperty.call(body, "systemPrompt");
323
+ const hasSystemInstructions =
324
+ Object.prototype.hasOwnProperty.call(body, "systemInstructions") ||
325
+ Object.prototype.hasOwnProperty.call(body, "systemPrompt");
227
326
  const systemInstructions = String(
228
327
  body.systemInstructions ?? body.systemPrompt ?? "",
229
328
  ).trim();
230
- if (!name) return res.status(400).json({ ok: false, error: "name is required" });
329
+ if (!name)
330
+ return res.status(400).json({ ok: false, error: "name is required" });
231
331
  try {
232
332
  const threadId = Number.parseInt(String(topicId), 10);
233
333
  if (!Number.isFinite(threadId)) {
234
- return res.status(400).json({ ok: false, error: "topicId must be numeric" });
334
+ return res
335
+ .status(400)
336
+ .json({ ok: false, error: "topicId must be numeric" });
235
337
  }
236
- const existingTopic = topicRegistry.getGroup(groupId)?.topics?.[String(threadId)] || {};
338
+ const existingTopic =
339
+ topicRegistry.getGroup(groupId)?.topics?.[String(threadId)] || {};
237
340
  const existingName = String(existingTopic.name || "").trim();
238
341
  const shouldRename = !existingName || existingName !== name;
239
342
  if (shouldRename) {
@@ -260,9 +363,15 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
260
363
  resolvedUserId: "",
261
364
  });
262
365
  syncPromptFiles();
366
+ const syncWarning = await runTelegramGitSync("rename-topic", name);
263
367
  return res.json({
264
368
  ok: true,
265
- topic: { threadId, name, ...(hasSystemInstructions ? { systemInstructions } : {}) },
369
+ topic: {
370
+ threadId,
371
+ name,
372
+ ...(hasSystemInstructions ? { systemInstructions } : {}),
373
+ },
374
+ syncWarning,
266
375
  });
267
376
  } catch (e) {
268
377
  return res.json({ ok: false, error: e.message });
@@ -296,8 +405,9 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
296
405
  topicRegistry.setGroup(groupId, { name: groupName });
297
406
  syncPromptFiles();
298
407
  }
408
+ const syncWarning = await runTelegramGitSync("configure-group", groupId);
299
409
 
300
- res.json({ ok: true, userId: resolvedUserId || null });
410
+ res.json({ ok: true, userId: resolvedUserId || null, syncWarning });
301
411
  } catch (e) {
302
412
  res.json({ ok: false, error: e.message });
303
413
  }
@@ -313,14 +423,24 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
313
423
  try {
314
424
  const debugEnabled = isDebugEnabled();
315
425
  const configPath = `${OPENCLAW_DIR}/openclaw.json`;
316
- const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
317
- const telegramConfig = cfg.channels?.telegram || {};
426
+ let cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
427
+ let telegramConfig = cfg.channels?.telegram || {};
318
428
  const configuredGroups = telegramConfig.groups || {};
319
429
  const groupIds = Object.keys(configuredGroups);
320
430
  if (groupIds.length === 0) {
321
431
  return res.json({ ok: true, configured: false, debugEnabled });
322
432
  }
323
433
  const groupId = String(groupIds[0]);
434
+ const groupConfig = configuredGroups[groupId] || {};
435
+ const repairResult = await repairGroupAllowFromIfMissing({
436
+ cfg,
437
+ groupId,
438
+ requireMention: !!groupConfig.requireMention,
439
+ });
440
+ if (repairResult.repaired) {
441
+ cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
442
+ telegramConfig = cfg.channels?.telegram || {};
443
+ }
324
444
  const registryGroup = topicRegistry.getGroup(groupId);
325
445
  let groupName = registryGroup?.name || groupId;
326
446
  try {
@@ -336,8 +456,12 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
336
456
  debugEnabled,
337
457
  concurrency: {
338
458
  agentMaxConcurrent: cfg.agents?.defaults?.maxConcurrent ?? null,
339
- subagentMaxConcurrent: cfg.agents?.defaults?.subagents?.maxConcurrent ?? null,
459
+ subagentMaxConcurrent:
460
+ cfg.agents?.defaults?.subagents?.maxConcurrent ?? null,
340
461
  },
462
+ repairedGroupAllowFrom: !!repairResult.repaired,
463
+ repairedUserId: repairResult.resolvedUserId || null,
464
+ syncWarning: repairResult.syncWarning || null,
341
465
  });
342
466
  } catch (e) {
343
467
  return res.json({ ok: false, error: e.message });
@@ -345,7 +469,7 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
345
469
  });
346
470
 
347
471
  // Reset Telegram workspace onboarding state
348
- app.post("/api/telegram/workspace/reset", (req, res) => {
472
+ app.post("/api/telegram/workspace/reset", async (req, res) => {
349
473
  try {
350
474
  const configPath = `${OPENCLAW_DIR}/openclaw.json`;
351
475
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
@@ -366,11 +490,12 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
366
490
  }
367
491
 
368
492
  syncPromptFiles();
369
- return res.json({ ok: true });
493
+ const syncWarning = await runTelegramGitSync("reset-workspace", "telegram");
494
+ return res.json({ ok: true, syncWarning });
370
495
  } catch (e) {
371
496
  return res.json({ ok: false, error: e.message });
372
497
  }
373
498
  });
374
499
  };
375
500
 
376
- module.exports = { registerTelegramRoutes };
501
+ module.exports = { registerTelegramRoutes, buildTelegramGitSyncCommand };
@@ -0,0 +1,133 @@
1
+ const topicRegistry = require("../topic-registry");
2
+
3
+ const kSummaryCacheTtlMs = 60 * 1000;
4
+
5
+ const parsePositiveInt = (value, fallbackValue) => {
6
+ const parsed = Number.parseInt(String(value ?? ""), 10);
7
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackValue;
8
+ };
9
+
10
+ const createSummaryCache = () => new Map();
11
+
12
+ // Parse "agent:main:telegram:group:-123:topic:42" into structured labels.
13
+ const parseSessionLabels = (sessionKey) => {
14
+ const raw = String(sessionKey || "").trim();
15
+ if (!raw) return null;
16
+ const parts = raw.split(":");
17
+ const labels = [];
18
+
19
+ if (parts[0] === "agent" && parts[1]) {
20
+ labels.push({
21
+ label: parts[1].charAt(0).toUpperCase() + parts[1].slice(1),
22
+ tone: "cyan",
23
+ });
24
+ }
25
+
26
+ const channelIndex = parts.indexOf("telegram");
27
+ if (channelIndex !== -1 && parts[channelIndex + 1]) {
28
+ const channelType = parts[channelIndex + 1];
29
+ if (channelType === "direct") {
30
+ labels.push({ label: "Telegram Direct", tone: "blue" });
31
+ } else if (channelType === "group") {
32
+ const groupId = parts[channelIndex + 2] || "";
33
+ let groupName = null;
34
+ let groupEntry = null;
35
+ try {
36
+ groupEntry = topicRegistry.getGroup(groupId);
37
+ groupName = groupEntry?.name || null;
38
+ } catch {}
39
+ labels.push({
40
+ label: groupName || `Group ${groupId}`,
41
+ tone: "purple",
42
+ });
43
+ const topicIndex = parts.indexOf("topic", channelIndex);
44
+ if (topicIndex !== -1 && parts[topicIndex + 1]) {
45
+ const topicId = parts[topicIndex + 1];
46
+ const topicName = groupEntry?.topics?.[topicId]?.name || null;
47
+ labels.push({
48
+ label: topicName || `Topic ${topicId}`,
49
+ tone: "gray",
50
+ });
51
+ }
52
+ } else {
53
+ labels.push({
54
+ label: `Telegram ${channelType.charAt(0).toUpperCase() + channelType.slice(1)}`,
55
+ tone: "blue",
56
+ });
57
+ }
58
+ }
59
+
60
+ return labels.length > 0 ? labels : null;
61
+ };
62
+
63
+ const enrichSessionLabels = (session) => ({
64
+ ...session,
65
+ labels: parseSessionLabels(session.sessionKey || session.sessionId),
66
+ });
67
+
68
+ const registerUsageRoutes = ({
69
+ app,
70
+ requireAuth,
71
+ getDailySummary,
72
+ getSessionsList,
73
+ getSessionDetail,
74
+ getSessionTimeSeries,
75
+ }) => {
76
+ const summaryCache = createSummaryCache();
77
+
78
+ app.get("/api/usage/summary", requireAuth, (req, res) => {
79
+ try {
80
+ const days = parsePositiveInt(req.query.days, 30);
81
+ const cacheKey = String(days);
82
+ const cached = summaryCache.get(cacheKey);
83
+ const now = Date.now();
84
+ if (cached && now - cached.cachedAt <= kSummaryCacheTtlMs) {
85
+ res.json({ ok: true, ...cached.payload, cached: true });
86
+ return;
87
+ }
88
+ const summary = getDailySummary({ days });
89
+ const payload = { summary };
90
+ summaryCache.set(cacheKey, { payload, cachedAt: now });
91
+ res.json({ ok: true, ...payload, cached: false });
92
+ } catch (err) {
93
+ res.status(500).json({ ok: false, error: err.message });
94
+ }
95
+ });
96
+
97
+ app.get("/api/usage/sessions", requireAuth, (req, res) => {
98
+ try {
99
+ const limit = parsePositiveInt(req.query.limit, 50);
100
+ const sessions = getSessionsList({ limit }).map(enrichSessionLabels);
101
+ res.json({ ok: true, sessions });
102
+ } catch (err) {
103
+ res.status(500).json({ ok: false, error: err.message });
104
+ }
105
+ });
106
+
107
+ app.get("/api/usage/sessions/:id", requireAuth, (req, res) => {
108
+ try {
109
+ const sessionId = String(req.params.id || "").trim();
110
+ const detail = getSessionDetail({ sessionId });
111
+ if (!detail) {
112
+ res.status(404).json({ ok: false, error: "Session not found" });
113
+ return;
114
+ }
115
+ res.json({ ok: true, detail: enrichSessionLabels(detail) });
116
+ } catch (err) {
117
+ res.status(500).json({ ok: false, error: err.message });
118
+ }
119
+ });
120
+
121
+ app.get("/api/usage/sessions/:id/timeseries", requireAuth, (req, res) => {
122
+ try {
123
+ const sessionId = String(req.params.id || "").trim();
124
+ const maxPoints = parsePositiveInt(req.query.maxPoints, 100);
125
+ const series = getSessionTimeSeries({ sessionId, maxPoints });
126
+ res.json({ ok: true, series });
127
+ } catch (err) {
128
+ res.status(500).json({ ok: false, error: err.message });
129
+ }
130
+ });
131
+ };
132
+
133
+ module.exports = { registerUsageRoutes };