@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
@@ -0,0 +1,397 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useState, useEffect } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+ import { showToast } from "../toast.js";
5
+ import { ActionButton } from "../action-button.js";
6
+ import { ConfirmDialog } from "../confirm-dialog.js";
7
+ import * as api from "../../lib/telegram-api.js";
8
+
9
+ const html = htm.bind(h);
10
+
11
+ export const ManageTelegramWorkspace = ({
12
+ groupId,
13
+ groupName,
14
+ initialTopics,
15
+ configAgentMaxConcurrent,
16
+ configSubagentMaxConcurrent,
17
+ debugEnabled,
18
+ onResetOnboarding,
19
+ }) => {
20
+ const [topics, setTopics] = useState(initialTopics || {});
21
+ const [newTopicName, setNewTopicName] = useState("");
22
+ const [newTopicInstructions, setNewTopicInstructions] = useState("");
23
+ const [showCreateTopic, setShowCreateTopic] = useState(false);
24
+ const [creating, setCreating] = useState(false);
25
+ const [deleting, setDeleting] = useState(null);
26
+ const [editingTopicId, setEditingTopicId] = useState("");
27
+ const [editingTopicName, setEditingTopicName] = useState("");
28
+ const [editingTopicInstructions, setEditingTopicInstructions] = useState("");
29
+ const [renamingTopicId, setRenamingTopicId] = useState("");
30
+ const [error, setError] = useState(null);
31
+ const [deleteTopicConfirm, setDeleteTopicConfirm] = useState(null);
32
+
33
+ const loadTopics = async () => {
34
+ const data = await api.listTopics(groupId);
35
+ if (data.ok) setTopics(data.topics || {});
36
+ };
37
+
38
+ useEffect(() => {
39
+ loadTopics();
40
+ }, [groupId]);
41
+ useEffect(() => {
42
+ if (initialTopics && Object.keys(initialTopics).length > 0) {
43
+ setTopics(initialTopics);
44
+ }
45
+ }, [initialTopics]);
46
+
47
+ const createSingle = async () => {
48
+ const name = newTopicName.trim();
49
+ const systemInstructions = newTopicInstructions.trim();
50
+ if (!name) return;
51
+ setCreating(true);
52
+ setError(null);
53
+ try {
54
+ const data = await api.createTopicsBulk(groupId, [
55
+ { name, ...(systemInstructions ? { systemInstructions } : {}) },
56
+ ]);
57
+ if (!data.ok)
58
+ throw new Error(data.results?.[0]?.error || "Failed to create topic");
59
+ const failed = data.results.filter((r) => !r.ok);
60
+ if (failed.length > 0) throw new Error(failed[0].error);
61
+ setNewTopicName("");
62
+ setNewTopicInstructions("");
63
+ setShowCreateTopic(false);
64
+ await loadTopics();
65
+ showToast(`Created topic: ${name}`, "success");
66
+ } catch (e) {
67
+ setError(e.message);
68
+ }
69
+ setCreating(false);
70
+ };
71
+
72
+ const handleDelete = async (topicId, topicName) => {
73
+ setDeleting(topicId);
74
+ try {
75
+ const data = await api.deleteTopic(groupId, topicId);
76
+ if (!data.ok) throw new Error(data.error);
77
+ await loadTopics();
78
+ if (data.removedFromRegistryOnly) {
79
+ showToast(`Removed stale topic from registry: ${topicName}`, "success");
80
+ } else {
81
+ showToast(`Deleted topic: ${topicName}`, "success");
82
+ }
83
+ } catch (e) {
84
+ showToast(`Failed to delete: ${e.message}`, "error");
85
+ }
86
+ setDeleting(null);
87
+ };
88
+
89
+ const startRename = (topicId, topicName, topicInstructions = "") => {
90
+ setEditingTopicId(String(topicId));
91
+ setEditingTopicName(String(topicName || ""));
92
+ setEditingTopicInstructions(String(topicInstructions || ""));
93
+ };
94
+
95
+ const cancelRename = () => {
96
+ setEditingTopicId("");
97
+ setEditingTopicName("");
98
+ setEditingTopicInstructions("");
99
+ };
100
+
101
+ const saveRename = async (topicId) => {
102
+ const nextName = editingTopicName.trim();
103
+ const nextSystemInstructions = editingTopicInstructions.trim();
104
+ if (!nextName) {
105
+ setError("Topic name is required");
106
+ return;
107
+ }
108
+ setRenamingTopicId(String(topicId));
109
+ setError(null);
110
+ try {
111
+ const data = await api.updateTopic(groupId, topicId, {
112
+ name: nextName,
113
+ systemInstructions: nextSystemInstructions,
114
+ });
115
+ if (!data.ok) throw new Error(data.error || "Failed to rename topic");
116
+ await loadTopics();
117
+ showToast(`Renamed topic: ${nextName}`, "success");
118
+ cancelRename();
119
+ } catch (e) {
120
+ setError(e.message);
121
+ }
122
+ setRenamingTopicId("");
123
+ };
124
+
125
+ const topicEntries = Object.entries(topics || {});
126
+ const topicCount = topicEntries.length;
127
+ const computedMaxConcurrent = Math.max(topicCount * 3, 8);
128
+ const computedSubagentMaxConcurrent = Math.max(computedMaxConcurrent - 2, 4);
129
+ const maxConcurrent = Number.isFinite(configAgentMaxConcurrent)
130
+ ? configAgentMaxConcurrent
131
+ : computedMaxConcurrent;
132
+ const subagentMaxConcurrent = Number.isFinite(configSubagentMaxConcurrent)
133
+ ? configSubagentMaxConcurrent
134
+ : computedSubagentMaxConcurrent;
135
+
136
+ return html`
137
+ <div class="space-y-4">
138
+ ${debugEnabled &&
139
+ html`
140
+ <div class="flex justify-end">
141
+ <button
142
+ onclick=${onResetOnboarding}
143
+ class="text-xs px-3 py-1.5 rounded-lg border border-border text-gray-400 hover:text-gray-200 hover:border-gray-500 transition-colors"
144
+ >
145
+ Reset onboarding
146
+ </button>
147
+ </div>
148
+ `}
149
+ <div class="bg-black/20 border border-border rounded-lg p-3 space-y-1">
150
+ <p class="text-sm text-gray-300 font-medium">${groupName || groupId}</p>
151
+ <p class="text-xs text-gray-500 font-mono">${groupId}</p>
152
+ </div>
153
+
154
+ <div class="space-y-2">
155
+ <h2 class="card-label mb-3">Existing Topics</h2>
156
+ ${topicEntries.length > 0
157
+ ? html`
158
+ <div
159
+ class="bg-black/20 border border-border rounded-lg overflow-hidden"
160
+ >
161
+ <table class="w-full text-xs table-fixed">
162
+ <thead>
163
+ <tr class="border-b border-border">
164
+ <th class="text-left px-3 py-2 text-gray-500 font-medium">
165
+ Topic
166
+ </th>
167
+ <th
168
+ class="text-left px-3 py-2 text-gray-500 font-medium w-36"
169
+ >
170
+ Thread ID
171
+ </th>
172
+ <th class="px-3 py-2 w-28" />
173
+ </tr>
174
+ </thead>
175
+ <tbody>
176
+ ${topicEntries.map(
177
+ ([id, topic]) => html`
178
+ ${editingTopicId === String(id)
179
+ ? html`
180
+ <tr
181
+ class="border-b border-border last:border-0 align-top"
182
+ >
183
+ <td class="px-3 py-2" colspan="3">
184
+ <div class="space-y-2">
185
+ <input
186
+ type="text"
187
+ value=${editingTopicName}
188
+ onInput=${(e) =>
189
+ setEditingTopicName(e.target.value)}
190
+ onKeyDown=${(e) => {
191
+ if (e.key === "Enter") saveRename(id);
192
+ if (e.key === "Escape") cancelRename();
193
+ }}
194
+ class="w-full bg-black/30 border border-border rounded-lg px-2 py-1.5 text-xs text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-500"
195
+ />
196
+ <textarea
197
+ value=${editingTopicInstructions}
198
+ onInput=${(e) =>
199
+ setEditingTopicInstructions(
200
+ e.target.value,
201
+ )}
202
+ placeholder="System instructions (optional)"
203
+ rows="6"
204
+ class="w-full bg-black/30 border border-border rounded-lg px-2 py-1.5 text-xs text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-500 resize-y"
205
+ />
206
+ <div class="flex items-center gap-2">
207
+ <button
208
+ onclick=${() => saveRename(id)}
209
+ disabled=${renamingTopicId ===
210
+ String(id)}
211
+ class="text-xs px-2 py-1 rounded transition-all ac-btn-cyan ${renamingTopicId ===
212
+ String(id)
213
+ ? "opacity-50 cursor-not-allowed"
214
+ : ""}"
215
+ >
216
+ Save
217
+ </button>
218
+ <button
219
+ onclick=${cancelRename}
220
+ class="text-xs px-2 py-1 rounded border border-border text-gray-400 hover:text-gray-200 hover:border-gray-500"
221
+ >
222
+ Cancel
223
+ </button>
224
+ </div>
225
+ </div>
226
+ </td>
227
+ </tr>
228
+ `
229
+ : html`
230
+ <tr
231
+ class="border-b border-border last:border-0 align-middle"
232
+ >
233
+ <td class="px-3 py-2 text-gray-300">
234
+ <div class="flex items-center gap-2">
235
+ <span>${topic.name}</span>
236
+ <button
237
+ onclick=${() =>
238
+ startRename(
239
+ id,
240
+ topic.name,
241
+ topic.systemInstructions,
242
+ )}
243
+ class="inline-flex items-center justify-center text-white/80 hover:text-white transition-colors"
244
+ title="Edit topic"
245
+ aria-label="Rename topic"
246
+ >
247
+ <svg
248
+ width="14"
249
+ height="14"
250
+ viewBox="0 0 16 16"
251
+ fill="currentColor"
252
+ aria-hidden="true"
253
+ >
254
+ <path
255
+ d="M11.854 1.146a.5.5 0 00-.708 0L3 9.293V13h3.707l8.146-8.146a.5.5 0 000-.708l-3-3zM3.5 12.5v-2.793l7-7L13.793 6l-7 7H3.5z"
256
+ />
257
+ </svg>
258
+ </button>
259
+ </div>
260
+ ${topic.systemInstructions &&
261
+ html`
262
+ <p
263
+ class="text-[11px] text-gray-500 mt-1 line-clamp-1"
264
+ >
265
+ ${topic.systemInstructions}
266
+ </p>
267
+ `}
268
+ </td>
269
+ <td
270
+ class="px-3 py-2 text-gray-500 font-mono w-36"
271
+ >
272
+ ${id}
273
+ </td>
274
+ <td class="px-3 py-2">
275
+ <div
276
+ class="flex items-center gap-2 justify-end"
277
+ >
278
+ <button
279
+ onclick=${() =>
280
+ setDeleteTopicConfirm({
281
+ id: String(id),
282
+ name: String(topic.name || ""),
283
+ })}
284
+ disabled=${deleting === id}
285
+ class="text-xs px-2 py-1 rounded border border-border text-gray-500 hover:text-red-300 hover:border-red-500 ${deleting ===
286
+ id
287
+ ? "opacity-50 cursor-not-allowed"
288
+ : ""}"
289
+ title="Delete topic"
290
+ >
291
+ Delete
292
+ </button>
293
+ </div>
294
+ </td>
295
+ </tr>
296
+ `}
297
+ `,
298
+ )}
299
+ </tbody>
300
+ </table>
301
+ </div>
302
+ `
303
+ : html`<p class="text-xs text-gray-500">No topics yet.</p>`}
304
+ </div>
305
+
306
+ ${showCreateTopic &&
307
+ html`
308
+ <div class="space-y-2 bg-black/20 border border-border rounded-lg p-3">
309
+ <label class="text-xs text-gray-500">Create new topic</label>
310
+ <div class="space-y-2">
311
+ <input
312
+ type="text"
313
+ value=${newTopicName}
314
+ onInput=${(e) => setNewTopicName(e.target.value)}
315
+ onKeyDown=${(e) => {
316
+ if (e.key === "Enter") createSingle();
317
+ }}
318
+ placeholder="Topic name"
319
+ class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-500"
320
+ />
321
+ <textarea
322
+ value=${newTopicInstructions}
323
+ onInput=${(e) => setNewTopicInstructions(e.target.value)}
324
+ placeholder="System instructions (optional)"
325
+ rows="5"
326
+ class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-500 resize-y"
327
+ />
328
+ <div class="flex justify-end">
329
+ <${ActionButton}
330
+ onClick=${createSingle}
331
+ disabled=${creating || !newTopicName.trim()}
332
+ loading=${creating}
333
+ tone="secondary"
334
+ size="lg"
335
+ idleLabel="Add topic"
336
+ loadingLabel="Creating..."
337
+ />
338
+ </div>
339
+ </div>
340
+ </div>
341
+ `}
342
+ ${error &&
343
+ html`
344
+ <div class="bg-red-500/10 border border-red-500/20 rounded-lg p-3">
345
+ <p class="text-sm text-red-400">${error}</p>
346
+ </div>
347
+ `}
348
+
349
+ <div class="flex items-center justify-start">
350
+ <button
351
+ onclick=${() => setShowCreateTopic((v) => !v)}
352
+ class="${showCreateTopic
353
+ ? "w-auto text-sm font-medium px-4 py-2 rounded-xl transition-all border border-border text-gray-300 hover:border-gray-500"
354
+ : "w-auto text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-cyan"}"
355
+ >
356
+ ${showCreateTopic ? "Close create topic" : "Create topic"}
357
+ </button>
358
+ </div>
359
+
360
+ <div class="border-t border-white/10" />
361
+
362
+ <p class="text-xs text-gray-500">
363
+ Concurrency is auto-scaled to support your group:
364
+ <span class="text-gray-300"> agent ${maxConcurrent}</span>,
365
+ <span class="text-gray-300"> subagent ${subagentMaxConcurrent}</span>
366
+ <span class="text-gray-600"> (${topicCount} topics)</span>.
367
+ </p>
368
+ <p class="text-[11px] text-gray-500">
369
+ This registry can drift if topics are created, renamed, or removed
370
+ outside this page. Your agent will update the registry if it notices a
371
+ discrepancy.
372
+ </p>
373
+ <${ConfirmDialog}
374
+ visible=${!!deleteTopicConfirm}
375
+ title="Delete topic?"
376
+ message=${deleteTopicConfirm
377
+ ? `This will delete "${deleteTopicConfirm.name}" (thread ${deleteTopicConfirm.id}) from your Telegram workspace.`
378
+ : "This will delete this topic from your Telegram workspace."}
379
+ confirmLabel="Delete topic"
380
+ confirmLoadingLabel="Deleting..."
381
+ confirmTone="warning"
382
+ confirmLoading=${!!deleting}
383
+ cancelLabel="Cancel"
384
+ onCancel=${() => {
385
+ if (deleting) return;
386
+ setDeleteTopicConfirm(null);
387
+ }}
388
+ onConfirm=${async () => {
389
+ if (!deleteTopicConfirm) return;
390
+ const pendingDelete = deleteTopicConfirm;
391
+ setDeleteTopicConfirm(null);
392
+ await handleDelete(pendingDelete.id, pendingDelete.name);
393
+ }}
394
+ />
395
+ </div>
396
+ `;
397
+ };