@chrysb/alphaclaw 0.1.25 → 0.2.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.
@@ -0,0 +1,65 @@
1
+ const kTelegramApiBase = "https://api.telegram.org";
2
+
3
+ const createTelegramApi = (getToken) => {
4
+ const call = async (method, params = {}) => {
5
+ const token = typeof getToken === "function" ? getToken() : getToken;
6
+ if (!token) throw new Error("TELEGRAM_BOT_TOKEN is not set");
7
+ const url = `${kTelegramApiBase}/bot${token}/${method}`;
8
+ const res = await fetch(url, {
9
+ method: "POST",
10
+ headers: { "Content-Type": "application/json" },
11
+ body: JSON.stringify(params),
12
+ });
13
+ const data = await res.json();
14
+ if (!data.ok) {
15
+ const err = new Error(data.description || `Telegram API error: ${method}`);
16
+ err.telegramErrorCode = data.error_code;
17
+ throw err;
18
+ }
19
+ return data.result;
20
+ };
21
+
22
+ const getMe = () => call("getMe");
23
+
24
+ const getChat = (chatId) => call("getChat", { chat_id: chatId });
25
+
26
+ const getChatMember = (chatId, userId) =>
27
+ call("getChatMember", { chat_id: chatId, user_id: userId });
28
+
29
+ const getChatAdministrators = (chatId) =>
30
+ call("getChatAdministrators", { chat_id: chatId });
31
+
32
+ const createForumTopic = (chatId, name, opts = {}) =>
33
+ call("createForumTopic", {
34
+ chat_id: chatId,
35
+ name,
36
+ ...(opts.iconColor != null && { icon_color: opts.iconColor }),
37
+ ...(opts.iconCustomEmojiId && { icon_custom_emoji_id: opts.iconCustomEmojiId }),
38
+ });
39
+
40
+ const deleteForumTopic = (chatId, messageThreadId) =>
41
+ call("deleteForumTopic", {
42
+ chat_id: chatId,
43
+ message_thread_id: messageThreadId,
44
+ });
45
+
46
+ const editForumTopic = (chatId, messageThreadId, opts = {}) =>
47
+ call("editForumTopic", {
48
+ chat_id: chatId,
49
+ message_thread_id: messageThreadId,
50
+ ...(opts.name && { name: opts.name }),
51
+ ...(opts.iconCustomEmojiId && { icon_custom_emoji_id: opts.iconCustomEmojiId }),
52
+ });
53
+
54
+ return {
55
+ getMe,
56
+ getChat,
57
+ getChatMember,
58
+ getChatAdministrators,
59
+ createForumTopic,
60
+ deleteForumTopic,
61
+ editForumTopic,
62
+ };
63
+ };
64
+
65
+ module.exports = { createTelegramApi };
@@ -0,0 +1,82 @@
1
+ const kTelegramTopicConcurrencyMultiplier = 3;
2
+ const kAgentConcurrencyFloor = 8;
3
+ const kSubagentConcurrencyFloor = 4;
4
+
5
+ const syncConfigForTelegram = ({
6
+ fs,
7
+ openclawDir,
8
+ topicRegistry,
9
+ groupId,
10
+ requireMention = false,
11
+ resolvedUserId = "",
12
+ }) => {
13
+ const configPath = `${openclawDir}/openclaw.json`;
14
+ const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
15
+
16
+ // Remove legacy root keys from older setup flow.
17
+ delete cfg.sessions;
18
+ delete cfg.groups;
19
+ delete cfg.groupAllowFrom;
20
+
21
+ if (!cfg.channels) cfg.channels = {};
22
+ if (!cfg.channels.telegram) cfg.channels.telegram = {};
23
+ if (!cfg.channels.telegram.groups) cfg.channels.telegram.groups = {};
24
+ const existingGroupConfig = cfg.channels.telegram.groups[groupId] || {};
25
+ cfg.channels.telegram.groups[groupId] = {
26
+ ...existingGroupConfig,
27
+ requireMention,
28
+ };
29
+
30
+ const registryTopics = topicRegistry.getGroup(groupId)?.topics || {};
31
+ const promptTopics = {};
32
+ for (const [threadId, topic] of Object.entries(registryTopics)) {
33
+ const systemPrompt = String(topic?.systemInstructions || "").trim();
34
+ if (!systemPrompt) continue;
35
+ promptTopics[threadId] = { systemPrompt };
36
+ }
37
+ if (Object.keys(promptTopics).length > 0) {
38
+ cfg.channels.telegram.groups[groupId].topics = promptTopics;
39
+ } else {
40
+ delete cfg.channels.telegram.groups[groupId].topics;
41
+ }
42
+
43
+ cfg.channels.telegram.groupPolicy = "allowlist";
44
+ if (!Array.isArray(cfg.channels.telegram.groupAllowFrom)) {
45
+ cfg.channels.telegram.groupAllowFrom = [];
46
+ }
47
+ if (
48
+ resolvedUserId
49
+ && !cfg.channels.telegram.groupAllowFrom.includes(String(resolvedUserId))
50
+ ) {
51
+ cfg.channels.telegram.groupAllowFrom.push(String(resolvedUserId));
52
+ }
53
+
54
+ // Persist thread sessions and keep concurrency in schema-valid agent defaults.
55
+ if (!cfg.session) cfg.session = {};
56
+ if (!cfg.session.resetByType) cfg.session.resetByType = {};
57
+ cfg.session.resetByType.thread = { mode: "idle", idleMinutes: 525600 };
58
+
59
+ const totalTopics = topicRegistry.getTotalTopicCount();
60
+ const maxConcurrent = Math.max(
61
+ totalTopics * kTelegramTopicConcurrencyMultiplier,
62
+ kAgentConcurrencyFloor,
63
+ );
64
+ if (!cfg.agents) cfg.agents = {};
65
+ if (!cfg.agents.defaults) cfg.agents.defaults = {};
66
+ cfg.agents.defaults.maxConcurrent = maxConcurrent;
67
+ if (!cfg.agents.defaults.subagents) cfg.agents.defaults.subagents = {};
68
+ cfg.agents.defaults.subagents.maxConcurrent = Math.max(
69
+ maxConcurrent - 2,
70
+ kSubagentConcurrencyFloor,
71
+ );
72
+
73
+ fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
74
+
75
+ return {
76
+ totalTopics,
77
+ maxConcurrent: cfg.agents.defaults.maxConcurrent,
78
+ subagentMaxConcurrent: cfg.agents.defaults.subagents.maxConcurrent,
79
+ };
80
+ };
81
+
82
+ module.exports = { syncConfigForTelegram };
@@ -0,0 +1,152 @@
1
+ const fs = require("fs");
2
+ const { WORKSPACE_DIR } = require("./constants");
3
+
4
+ const kRegistryPath = `${WORKSPACE_DIR}/topic-registry.json`;
5
+
6
+ const readRegistry = () => {
7
+ try {
8
+ return JSON.parse(fs.readFileSync(kRegistryPath, "utf8"));
9
+ } catch {
10
+ return { groups: {} };
11
+ }
12
+ };
13
+
14
+ const writeRegistry = (registry) => {
15
+ fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
16
+ fs.writeFileSync(kRegistryPath, JSON.stringify(registry, null, 2));
17
+ };
18
+
19
+ const getGroup = (groupId) => {
20
+ const registry = readRegistry();
21
+ return registry.groups[groupId] || null;
22
+ };
23
+
24
+ const setGroup = (groupId, groupData) => {
25
+ const registry = readRegistry();
26
+ const existingGroup = registry.groups[groupId] || {
27
+ name: groupId,
28
+ topics: {},
29
+ };
30
+ registry.groups[groupId] = {
31
+ ...existingGroup,
32
+ ...groupData,
33
+ topics: existingGroup.topics || {},
34
+ };
35
+ writeRegistry(registry);
36
+ return registry;
37
+ };
38
+
39
+ const addTopic = (groupId, threadId, topicData) => {
40
+ const registry = readRegistry();
41
+ if (!registry.groups[groupId]) {
42
+ registry.groups[groupId] = { name: groupId, topics: {} };
43
+ }
44
+ if (
45
+ !registry.groups[groupId].topics ||
46
+ typeof registry.groups[groupId].topics !== "object"
47
+ ) {
48
+ registry.groups[groupId].topics = {};
49
+ }
50
+ registry.groups[groupId].topics[String(threadId)] = topicData;
51
+ writeRegistry(registry);
52
+ return registry;
53
+ };
54
+
55
+ const updateTopic = (groupId, threadId, topicData) => {
56
+ const registry = readRegistry();
57
+ if (!registry.groups[groupId]) {
58
+ registry.groups[groupId] = { name: groupId, topics: {} };
59
+ }
60
+ if (
61
+ !registry.groups[groupId].topics ||
62
+ typeof registry.groups[groupId].topics !== "object"
63
+ ) {
64
+ registry.groups[groupId].topics = {};
65
+ }
66
+ const existing = registry.groups[groupId].topics[String(threadId)] || {};
67
+ registry.groups[groupId].topics[String(threadId)] = {
68
+ ...existing,
69
+ ...topicData,
70
+ };
71
+ writeRegistry(registry);
72
+ return registry;
73
+ };
74
+
75
+ const removeTopic = (groupId, threadId) => {
76
+ const registry = readRegistry();
77
+ if (registry.groups[groupId]?.topics) {
78
+ delete registry.groups[groupId].topics[String(threadId)];
79
+ }
80
+ writeRegistry(registry);
81
+ return registry;
82
+ };
83
+
84
+ const getTotalTopicCount = () => {
85
+ const registry = readRegistry();
86
+ let count = 0;
87
+ for (const group of Object.values(registry.groups)) {
88
+ count += Object.keys(group.topics || {}).length;
89
+ }
90
+ return count;
91
+ };
92
+
93
+ // Render the topic registry as a markdown section for TOOLS.md
94
+ const renderTopicRegistryMarkdown = ({ includeSyncGuidance = false } = {}) => {
95
+ const registry = readRegistry();
96
+ const rows = [];
97
+ for (const [groupId, group] of Object.entries(registry.groups)) {
98
+ for (const [threadId, topic] of Object.entries(group.topics || {})) {
99
+ rows.push({
100
+ groupName: group.name || groupId,
101
+ groupId,
102
+ topicName: topic.name,
103
+ threadId,
104
+ });
105
+ }
106
+ }
107
+ if (rows.length === 0 && !includeSyncGuidance) return "";
108
+
109
+ const lines = [
110
+ "",
111
+ "## Topic Registry",
112
+ "",
113
+ "When sending messages to group topics, use these thread IDs:",
114
+ "",
115
+ "| Group | Topic | Thread ID |",
116
+ "| ----- | ----- | --------- |",
117
+ ];
118
+ for (const r of rows) {
119
+ lines.push(
120
+ `| ${r.groupName} (${r.groupId}) | ${r.topicName} | ${r.threadId} |`,
121
+ );
122
+ }
123
+ if (includeSyncGuidance) {
124
+ lines.push(
125
+ "",
126
+ "### Sync Rules",
127
+ "",
128
+ "When Telegram workspace is enabled, keep topic mappings in sync with real Telegram activity:",
129
+ "",
130
+ "- If a message arrives in an unregistered Telegram topic, ask the user to name it for addition to the registry.",
131
+ '- When adding a topic (new or missing) run `alphaclaw telegram topic add --thread <threadId> --name "<topicName>"` immediately, no confirmation needed.',
132
+ "- Never edit `hooks/bootstrap/TOOLS.md` directly for topic changes",
133
+ "",
134
+ );
135
+ } else {
136
+ lines.push("");
137
+ }
138
+ return lines.join("\n");
139
+ };
140
+
141
+ module.exports = {
142
+ kRegistryPath,
143
+ readRegistry,
144
+ writeRegistry,
145
+ getGroup,
146
+ setGroup,
147
+ addTopic,
148
+ updateTopic,
149
+ removeTopic,
150
+ getTotalTopicCount,
151
+ renderTopicRegistryMarkdown,
152
+ };
package/lib/server.js CHANGED
@@ -36,6 +36,7 @@ const { createLoginThrottle } = require("./server/login-throttle");
36
36
  const { createOpenclawVersionService } = require("./server/openclaw-version");
37
37
  const { createAlphaclawVersionService } = require("./server/alphaclaw-version");
38
38
  const { syncBootstrapPromptFiles } = require("./server/onboarding/workspace");
39
+ const { createTelegramApi } = require("./server/telegram-api");
39
40
 
40
41
  const { registerAuthRoutes } = require("./server/routes/auth");
41
42
  const { registerPageRoutes } = require("./server/routes/pages");
@@ -46,6 +47,7 @@ const { registerPairingRoutes } = require("./server/routes/pairings");
46
47
  const { registerCodexRoutes } = require("./server/routes/codex");
47
48
  const { registerGoogleRoutes } = require("./server/routes/google");
48
49
  const { registerProxyRoutes } = require("./server/routes/proxy");
50
+ const { registerTelegramRoutes } = require("./server/routes/telegram");
49
51
 
50
52
  const { PORT, GATEWAY_URL, kTrustProxyHops, SETUP_API_PREFIXES } = constants;
51
53
 
@@ -146,6 +148,10 @@ registerGoogleRoutes({
146
148
  getApiEnableUrl,
147
149
  constants,
148
150
  });
151
+ const telegramApi = createTelegramApi(() => process.env.TELEGRAM_BOT_TOKEN);
152
+ const doSyncPromptFiles = () =>
153
+ syncBootstrapPromptFiles({ fs, workspaceDir: constants.WORKSPACE_DIR });
154
+ registerTelegramRoutes({ app, telegramApi, syncPromptFiles: doSyncPromptFiles });
149
155
  registerProxyRoutes({ app, proxy, SETUP_API_PREFIXES, requireAuth });
150
156
 
151
157
  const server = http.createServer(app);
@@ -170,7 +176,7 @@ server.on("upgrade", (req, socket, head) => {
170
176
 
171
177
  server.listen(PORT, "0.0.0.0", () => {
172
178
  console.log(`[alphaclaw] Express listening on :${PORT}`);
173
- syncBootstrapPromptFiles({ fs, workspaceDir: constants.WORKSPACE_DIR });
179
+ doSyncPromptFiles();
174
180
  if (isOnboarded()) {
175
181
  reloadEnv();
176
182
  syncChannelConfig(readEnvFile());
@@ -25,7 +25,7 @@ Google Workspace is connected via the **General** tab (`{{SETUP_UI_URL}}#general
25
25
  **Commit and push after every set of changes.** Your entire .openclaw directory (config, cron, workspace) is version controlled. This is how your work survives container restarts.
26
26
 
27
27
  ```bash
28
- cd /data/.openclaw && git add -A && git commit -m "description" && git push
28
+ alphaclaw git-sync --message "description"
29
29
  ```
30
30
 
31
31
  Never force push. Always pull before pushing if there might be remote changes.
@@ -11,6 +11,9 @@ GEMINI_API_KEY=
11
11
  GITHUB_TOKEN=
12
12
  GITHUB_WORKSPACE_REPO=
13
13
 
14
+ # --- Setup UI auth (required) ---
15
+ SETUP_PASSWORD=
16
+
14
17
  # --- Channels (at least one required) ---
15
18
  TELEGRAM_BOT_TOKEN=
16
19
  DISCORD_BOT_TOKEN=
@@ -4,6 +4,14 @@ set -euo pipefail
4
4
  REPO="$(cd "$(dirname "$0")" && pwd)"
5
5
  cd "$REPO"
6
6
 
7
+ # Load persisted env vars when running under cron's minimal environment.
8
+ if [[ -f "$REPO/.env" ]]; then
9
+ set -a
10
+ # shellcheck disable=SC1091
11
+ source "$REPO/.env"
12
+ set +a
13
+ fi
14
+
7
15
  # Drop cron scheduler runtime-only churn when it is metadata/timestamp-only.
8
16
  maybe_restore_if_runtime_only() {
9
17
  local file="$1"
@@ -73,14 +81,5 @@ NODE
73
81
  maybe_restore_if_runtime_only "cron/jobs.json"
74
82
  maybe_restore_if_runtime_only "crons.json"
75
83
 
76
- # Stage everything else.
77
- git add -A
78
-
79
- # Nothing to commit? done.
80
- if git diff --cached --quiet; then
81
- exit 0
82
- fi
83
-
84
84
  msg="Auto-commit hourly sync $(date -u +'%Y-%m-%dT%H:%M:%SZ')"
85
- git commit -m "$msg"
86
- git push
85
+ alphaclaw git-sync -m "$msg"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.1.25",
3
+ "version": "0.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },