@chrysb/alphaclaw 0.5.5 → 0.5.7-beta.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 (86) hide show
  1. package/bin/alphaclaw.js +6 -1
  2. package/lib/public/css/agents.css +92 -0
  3. package/lib/public/css/explorer.css +101 -0
  4. package/lib/public/css/shell.css +15 -4
  5. package/lib/public/js/app.js +69 -3
  6. package/lib/public/js/components/action-button.js +5 -0
  7. package/lib/public/js/components/agents-tab/agent-bindings-section/helpers.js +76 -0
  8. package/lib/public/js/components/agents-tab/agent-bindings-section/index.js +490 -0
  9. package/lib/public/js/components/agents-tab/agent-bindings-section/use-agent-bindings.js +256 -0
  10. package/lib/public/js/components/agents-tab/agent-detail-panel.js +74 -0
  11. package/lib/public/js/components/agents-tab/agent-identity-section.js +175 -0
  12. package/lib/public/js/components/agents-tab/agent-overview/index.js +53 -0
  13. package/lib/public/js/components/agents-tab/agent-overview/manage-card.js +44 -0
  14. package/lib/public/js/components/agents-tab/agent-overview/model-card.js +158 -0
  15. package/lib/public/js/components/agents-tab/agent-overview/use-model-card.js +169 -0
  16. package/lib/public/js/components/agents-tab/agent-overview/use-workspace-card.js +45 -0
  17. package/lib/public/js/components/agents-tab/agent-overview/workspace-card.js +47 -0
  18. package/lib/public/js/components/agents-tab/agent-pairing-section.js +265 -0
  19. package/lib/public/js/components/agents-tab/create-agent-modal.js +189 -0
  20. package/lib/public/js/components/agents-tab/create-channel-modal.js +323 -0
  21. package/lib/public/js/components/agents-tab/delete-agent-dialog.js +50 -0
  22. package/lib/public/js/components/agents-tab/edit-agent-modal.js +109 -0
  23. package/lib/public/js/components/agents-tab/index.js +148 -0
  24. package/lib/public/js/components/agents-tab/use-agents.js +89 -0
  25. package/lib/public/js/components/channel-account-status-badge.js +35 -0
  26. package/lib/public/js/components/channel-operations-panel.js +33 -0
  27. package/lib/public/js/components/channels.js +545 -60
  28. package/lib/public/js/components/envars.js +25 -4
  29. package/lib/public/js/components/general/index.js +21 -11
  30. package/lib/public/js/components/general/use-general-tab.js +78 -16
  31. package/lib/public/js/components/google/gmail-setup-wizard.js +1 -3
  32. package/lib/public/js/components/google/index.js +28 -30
  33. package/lib/public/js/components/icons.js +37 -0
  34. package/lib/public/js/components/models-tab/index.js +58 -224
  35. package/lib/public/js/components/models-tab/model-picker.js +212 -0
  36. package/lib/public/js/components/models-tab/use-models.js +17 -14
  37. package/lib/public/js/components/onboarding/use-welcome-pairing.js +4 -4
  38. package/lib/public/js/components/onboarding/welcome-pairing-step.js +2 -2
  39. package/lib/public/js/components/overflow-menu.js +122 -0
  40. package/lib/public/js/components/pairings.js +36 -8
  41. package/lib/public/js/components/routes/agents-route.js +27 -0
  42. package/lib/public/js/components/routes/general-route.js +2 -0
  43. package/lib/public/js/components/routes/index.js +1 -0
  44. package/lib/public/js/components/routes/telegram-route.js +2 -2
  45. package/lib/public/js/components/secret-input.js +8 -1
  46. package/lib/public/js/components/sidebar.js +65 -39
  47. package/lib/public/js/components/telegram-workspace/index.js +175 -74
  48. package/lib/public/js/components/telegram-workspace/manage.js +83 -10
  49. package/lib/public/js/components/telegram-workspace/onboarding.js +9 -8
  50. package/lib/public/js/components/webhooks.js +43 -18
  51. package/lib/public/js/hooks/use-app-shell-controller.js +7 -0
  52. package/lib/public/js/hooks/use-browse-navigation.js +8 -5
  53. package/lib/public/js/hooks/use-destination-session-selection.js +8 -1
  54. package/lib/public/js/lib/api.js +163 -9
  55. package/lib/public/js/lib/app-navigation.js +2 -1
  56. package/lib/public/js/lib/channel-create-operation.js +102 -0
  57. package/lib/public/js/lib/format.js +14 -0
  58. package/lib/public/js/lib/sse.js +51 -0
  59. package/lib/public/js/lib/telegram-api.js +38 -18
  60. package/lib/public/setup.html +1 -0
  61. package/lib/public/shared/browse-file-policies.json +0 -1
  62. package/lib/server/agents/service.js +1478 -0
  63. package/lib/server/constants.js +2 -2
  64. package/lib/server/env.js +3 -1
  65. package/lib/server/gateway.js +104 -20
  66. package/lib/server/gmail-serve.js +2 -12
  67. package/lib/server/gmail-watch.js +29 -2
  68. package/lib/server/onboarding/import/import-applier.js +0 -1
  69. package/lib/server/onboarding/index.js +0 -6
  70. package/lib/server/onboarding/workspace.js +74 -38
  71. package/lib/server/openclaw-config.js +23 -0
  72. package/lib/server/operation-events.js +141 -0
  73. package/lib/server/routes/agents.js +266 -0
  74. package/lib/server/routes/pairings.js +135 -25
  75. package/lib/server/routes/system.js +90 -10
  76. package/lib/server/routes/telegram.js +247 -51
  77. package/lib/server/startup.js +23 -0
  78. package/lib/server/telegram-workspace.js +61 -10
  79. package/lib/server/topic-registry.js +66 -7
  80. package/lib/server/watchdog.js +151 -27
  81. package/lib/server/webhooks.js +60 -12
  82. package/lib/server.js +40 -27
  83. package/lib/setup/core-prompts/AGENTS.md +6 -5
  84. package/lib/setup/core-prompts/TOOLS.md +1 -8
  85. package/package.json +1 -1
  86. package/lib/setup/skills/control-ui/SKILL.md +0 -62
@@ -0,0 +1,266 @@
1
+ const parseKeepWorkspace = (value) => {
2
+ if (value === undefined || value === null) return true;
3
+ const normalized = String(value).trim().toLowerCase();
4
+ if (!normalized) return true;
5
+ return !["0", "false", "no", "off"].includes(normalized);
6
+ };
7
+
8
+ const registerAgentRoutes = ({
9
+ app,
10
+ agentsService,
11
+ restartRequiredState = null,
12
+ operationEvents = null,
13
+ }) => {
14
+ app.get("/api/channels/accounts", (_req, res) => {
15
+ try {
16
+ res.json({
17
+ ok: true,
18
+ channels: agentsService.listConfiguredChannelAccounts(),
19
+ });
20
+ } catch (error) {
21
+ res.status(500).json({ ok: false, error: error.message });
22
+ }
23
+ });
24
+
25
+ app.get("/api/channels/accounts/token", (req, res) => {
26
+ try {
27
+ const provider = String(req.query?.provider || "").trim();
28
+ const accountId = String(req.query?.accountId || "").trim() || "default";
29
+ const result = agentsService.getChannelAccountToken({
30
+ provider,
31
+ accountId,
32
+ });
33
+ return res.json({ ok: true, ...result });
34
+ } catch (error) {
35
+ const status = String(error.message || "").includes("not found")
36
+ ? 404
37
+ : 400;
38
+ return res.status(status).json({ ok: false, error: error.message });
39
+ }
40
+ });
41
+
42
+ app.post("/api/channels/accounts", async (req, res) => {
43
+ try {
44
+ const body = req.body || {};
45
+ const result = await agentsService.createChannelAccount(body);
46
+ return res.status(201).json({ ok: true, ...result });
47
+ } catch (error) {
48
+ const message = String(error.message || "");
49
+ const status = message.includes("already exists")
50
+ ? 409
51
+ : message.includes("already assigned")
52
+ ? 409
53
+ : message.includes("not found")
54
+ ? 404
55
+ : 400;
56
+ return res.status(status).json({ ok: false, error: error.message });
57
+ }
58
+ });
59
+
60
+ app.post("/api/channels/accounts/jobs", (req, res) => {
61
+ if (!operationEvents?.createOperation) {
62
+ return res
63
+ .status(503)
64
+ .json({ ok: false, error: "Operation events unavailable" });
65
+ }
66
+ const body = req.body || {};
67
+ const { operationId } = operationEvents.createOperation({
68
+ type: "channel-account-create",
69
+ });
70
+ (async () => {
71
+ try {
72
+ const result = await agentsService.createChannelAccount(body, {
73
+ onProgress: ({ phase = "", label = "" } = {}) => {
74
+ operationEvents.publish(operationId, {
75
+ event: "phase",
76
+ data: {
77
+ phase: String(phase || "").trim(),
78
+ label: String(label || "").trim(),
79
+ },
80
+ });
81
+ },
82
+ });
83
+ operationEvents.complete(operationId, { ok: true, ...result });
84
+ } catch (error) {
85
+ operationEvents.fail(operationId, error);
86
+ }
87
+ })();
88
+ return res.status(202).json({
89
+ ok: true,
90
+ operationId,
91
+ streamUrl: `/api/operations/${encodeURIComponent(operationId)}/events`,
92
+ });
93
+ });
94
+
95
+ app.get("/api/operations/:operationId/events", (req, res) => {
96
+ if (!operationEvents?.subscribe) {
97
+ return res
98
+ .status(503)
99
+ .json({ ok: false, error: "Operation events unavailable" });
100
+ }
101
+ const subscribed = operationEvents.subscribe({
102
+ operationId: req.params.operationId,
103
+ req,
104
+ res,
105
+ });
106
+ if (!subscribed) {
107
+ return res.status(404).json({ ok: false, error: "Operation not found" });
108
+ }
109
+ });
110
+
111
+ app.put("/api/channels/accounts", (req, res) => {
112
+ try {
113
+ const result = agentsService.updateChannelAccount(req.body || {});
114
+ const restartRequired = !!result?.tokenUpdated;
115
+ if (restartRequired) {
116
+ restartRequiredState?.markRequired?.("channel_token_updated");
117
+ }
118
+ return res.json({ ok: true, restartRequired, ...result });
119
+ } catch (error) {
120
+ const status = String(error.message || "").includes("not found")
121
+ ? 404
122
+ : 400;
123
+ return res.status(status).json({ ok: false, error: error.message });
124
+ }
125
+ });
126
+
127
+ app.delete("/api/channels/accounts", async (req, res) => {
128
+ try {
129
+ const body = req.body || {};
130
+ await agentsService.deleteChannelAccount(body);
131
+ return res.json({ ok: true });
132
+ } catch (error) {
133
+ const status = String(error.message || "").includes("not found")
134
+ ? 404
135
+ : 400;
136
+ return res.status(status).json({ ok: false, error: error.message });
137
+ }
138
+ });
139
+
140
+ app.get("/api/agents", (_req, res) => {
141
+ try {
142
+ res.json({ ok: true, agents: agentsService.listAgents() });
143
+ } catch (error) {
144
+ res.status(500).json({ ok: false, error: error.message });
145
+ }
146
+ });
147
+
148
+ app.get("/api/agents/:id", (req, res) => {
149
+ try {
150
+ const agent = agentsService.getAgent(req.params.id);
151
+ if (!agent)
152
+ return res.status(404).json({ ok: false, error: "Agent not found" });
153
+ return res.json({ ok: true, agent });
154
+ } catch (error) {
155
+ return res.status(500).json({ ok: false, error: error.message });
156
+ }
157
+ });
158
+
159
+ app.get("/api/agents/:id/workspace-size", (req, res) => {
160
+ try {
161
+ const workspace = agentsService.getAgentWorkspaceSize(req.params.id);
162
+ return res.json({ ok: true, ...workspace });
163
+ } catch (error) {
164
+ const status = String(error.message || "").includes("not found")
165
+ ? 404
166
+ : 500;
167
+ return res.status(status).json({ ok: false, error: error.message });
168
+ }
169
+ });
170
+
171
+ app.get("/api/agents/:id/bindings", (req, res) => {
172
+ try {
173
+ const agent = agentsService.getAgent(req.params.id);
174
+ if (!agent)
175
+ return res.status(404).json({ ok: false, error: "Agent not found" });
176
+ return res.json({
177
+ ok: true,
178
+ bindings: agentsService.getBindingsForAgent(req.params.id),
179
+ });
180
+ } catch (error) {
181
+ return res.status(500).json({ ok: false, error: error.message });
182
+ }
183
+ });
184
+
185
+ app.post("/api/agents", (req, res) => {
186
+ try {
187
+ const body = req.body || {};
188
+ if (!String(body.id || "").trim()) {
189
+ return res.status(400).json({ ok: false, error: "id is required" });
190
+ }
191
+ const agent = agentsService.createAgent(body);
192
+ return res.status(201).json({ ok: true, agent });
193
+ } catch (error) {
194
+ const status = String(error.message || "").includes("already exists")
195
+ ? 409
196
+ : 400;
197
+ return res.status(status).json({ ok: false, error: error.message });
198
+ }
199
+ });
200
+
201
+ app.put("/api/agents/:id", (req, res) => {
202
+ try {
203
+ const agent = agentsService.updateAgent(req.params.id, req.body || {});
204
+ return res.json({ ok: true, agent });
205
+ } catch (error) {
206
+ const status = String(error.message || "").includes("not found")
207
+ ? 404
208
+ : 400;
209
+ return res.status(status).json({ ok: false, error: error.message });
210
+ }
211
+ });
212
+
213
+ app.post("/api/agents/:id/bindings", (req, res) => {
214
+ try {
215
+ const binding = agentsService.addBinding(req.params.id, req.body || {});
216
+ return res.status(201).json({ ok: true, binding });
217
+ } catch (error) {
218
+ const message = String(error.message || "");
219
+ const status = message.includes("not found")
220
+ ? 404
221
+ : message.includes("already assigned")
222
+ ? 409
223
+ : 400;
224
+ return res.status(status).json({ ok: false, error: error.message });
225
+ }
226
+ });
227
+
228
+ app.delete("/api/agents/:id/bindings", (req, res) => {
229
+ try {
230
+ agentsService.removeBinding(req.params.id, req.body || {});
231
+ return res.json({ ok: true });
232
+ } catch (error) {
233
+ const status = String(error.message || "").includes("not found")
234
+ ? 404
235
+ : 400;
236
+ return res.status(status).json({ ok: false, error: error.message });
237
+ }
238
+ });
239
+
240
+ app.delete("/api/agents/:id", (req, res) => {
241
+ try {
242
+ const keepWorkspace = parseKeepWorkspace(req.query.keepWorkspace);
243
+ agentsService.deleteAgent(req.params.id, { keepWorkspace });
244
+ return res.json({ ok: true });
245
+ } catch (error) {
246
+ const status = String(error.message || "").includes("not found")
247
+ ? 404
248
+ : 400;
249
+ return res.status(status).json({ ok: false, error: error.message });
250
+ }
251
+ });
252
+
253
+ app.post("/api/agents/:id/default", (req, res) => {
254
+ try {
255
+ const agent = agentsService.setDefaultAgent(req.params.id);
256
+ return res.json({ ok: true, agent });
257
+ } catch (error) {
258
+ const status = String(error.message || "").includes("not found")
259
+ ? 404
260
+ : 400;
261
+ return res.status(status).json({ ok: false, error: error.message });
262
+ }
263
+ });
264
+ };
265
+
266
+ module.exports = { registerAgentRoutes };
@@ -1,6 +1,61 @@
1
1
  const fs = require("fs");
2
+ const path = require("path");
2
3
  const { OPENCLAW_DIR } = require("../constants");
3
4
  const { buildManagedPaths } = require("../internal-files-migration");
5
+ const { parseJsonObjectFromNoisyOutput } = require("../utils/json");
6
+
7
+ const resolvePairingStorePath = ({ openclawDir, channel }) =>
8
+ path.join(openclawDir, "credentials", `${String(channel).trim().toLowerCase()}-pairing.json`);
9
+
10
+ const readPairingStore = ({ fsModule, filePath }) => {
11
+ try {
12
+ const raw = fsModule.readFileSync(filePath, "utf8");
13
+ const parsed = JSON.parse(raw);
14
+ return Array.isArray(parsed?.requests) ? parsed.requests : [];
15
+ } catch {
16
+ return [];
17
+ }
18
+ };
19
+
20
+ const writePairingStore = ({ fsModule, filePath, requests }) => {
21
+ fsModule.mkdirSync(path.dirname(filePath), { recursive: true });
22
+ fsModule.writeFileSync(filePath, JSON.stringify({ version: 1, requests }, null, 2));
23
+ };
24
+
25
+ const removeRequestFromPairingStore = ({ fsModule, openclawDir, channel, code, accountId }) => {
26
+ const filePath = resolvePairingStorePath({ openclawDir, channel });
27
+ const requests = readPairingStore({ fsModule, filePath });
28
+ const normalizedCode = String(code || "").trim().toUpperCase();
29
+ const normalizedAccountId = String(accountId || "").trim().toLowerCase();
30
+ const nextRequests = requests.filter((entry) => {
31
+ const entryCode = String(entry?.code || "").trim().toUpperCase();
32
+ if (entryCode !== normalizedCode) return true;
33
+ if (normalizedAccountId) {
34
+ const entryAccountId = String(entry?.meta?.accountId || "").trim().toLowerCase();
35
+ return entryAccountId !== normalizedAccountId;
36
+ }
37
+ return false;
38
+ });
39
+ if (nextRequests.length !== requests.length) {
40
+ writePairingStore({ fsModule, filePath, requests: nextRequests });
41
+ return true;
42
+ }
43
+ return false;
44
+ };
45
+
46
+ const removeAccountRequestsFromPairingStore = ({ fsModule, openclawDir, channel, accountId }) => {
47
+ const filePath = resolvePairingStorePath({ openclawDir, channel });
48
+ const requests = readPairingStore({ fsModule, filePath });
49
+ if (requests.length === 0) return;
50
+ const normalizedAccountId = String(accountId || "").trim().toLowerCase() || "default";
51
+ const nextRequests = requests.filter((entry) => {
52
+ const entryAccountId = String(entry?.meta?.accountId || "").trim().toLowerCase() || "default";
53
+ return entryAccountId !== normalizedAccountId;
54
+ });
55
+ if (nextRequests.length !== requests.length) {
56
+ writePairingStore({ fsModule, filePath, requests: nextRequests });
57
+ }
58
+ };
4
59
 
5
60
  const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openclawDir = OPENCLAW_DIR }) => {
6
61
  let pairingCache = { pending: [], ts: 0 };
@@ -22,6 +77,29 @@ const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openc
22
77
  );
23
78
  };
24
79
 
80
+ const parsePendingPairings = (stdout, channel) => {
81
+ const parsed = parseJsonObjectFromNoisyOutput(stdout) || {};
82
+ const requestLists = [
83
+ ...(Array.isArray(parsed?.requests) ? [parsed.requests] : []),
84
+ ...(Array.isArray(parsed?.pending) ? [parsed.pending] : []),
85
+ ];
86
+ return requestLists
87
+ .flat()
88
+ .map((entry) => {
89
+ const code = String(entry?.code || entry?.pairingCode || "").trim().toUpperCase();
90
+ if (!code) return null;
91
+ return {
92
+ id: code,
93
+ code,
94
+ channel: String(channel || "").trim(),
95
+ accountId:
96
+ String(entry?.meta?.accountId || entry?.accountId || "").trim() || "default",
97
+ requesterId: String(entry?.id || entry?.requesterId || "").trim(),
98
+ };
99
+ })
100
+ .filter(Boolean);
101
+ };
102
+
25
103
  app.get("/api/pairings", async (req, res) => {
26
104
  if (Date.now() - pairingCache.ts < PAIRING_CACHE_TTL) {
27
105
  return res.json({ pending: pairingCache.pending });
@@ -32,24 +110,20 @@ const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openc
32
110
 
33
111
  for (const ch of channels) {
34
112
  try {
35
- const config = JSON.parse(fs.readFileSync(`${OPENCLAW_DIR}/openclaw.json`, "utf8"));
113
+ const config = JSON.parse(
114
+ fsModule.readFileSync(`${openclawDir}/openclaw.json`, "utf8"),
115
+ );
36
116
  if (!config.channels?.[ch]?.enabled) continue;
37
117
  } catch {
38
118
  continue;
39
119
  }
40
120
 
41
- const result = await clawCmd(`pairing list ${ch}`, { quiet: true });
121
+ const result = await clawCmd(`pairing list --channel ${ch} --json`, { quiet: true });
42
122
  if (result.ok && result.stdout) {
43
- const lines = result.stdout.split("\n").filter((l) => l.trim());
44
- for (const line of lines) {
45
- const codeMatch = line.match(/([A-Z0-9]{8})/);
46
- if (codeMatch) {
47
- pending.push({
48
- id: codeMatch[1],
49
- code: codeMatch[1],
50
- channel: ch,
51
- });
52
- }
123
+ try {
124
+ pending.push(...parsePendingPairings(result.stdout, ch));
125
+ } catch {
126
+ // Ignore malformed output for a single channel and keep the rest of the response.
53
127
  }
54
128
  }
55
129
  }
@@ -60,26 +134,58 @@ const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openc
60
134
 
61
135
  app.post("/api/pairings/:id/approve", async (req, res) => {
62
136
  const channel = req.body.channel || "telegram";
63
- const result = await clawCmd(`pairing approve ${channel} ${req.params.id}`);
137
+ const accountId = String(req.body?.accountId || "").trim();
138
+ const approveCmd = accountId
139
+ ? `pairing approve --channel ${channel} --account ${accountId} ${req.params.id}`
140
+ : `pairing approve ${channel} ${req.params.id}`;
141
+ const result = await clawCmd(approveCmd);
64
142
  res.json(result);
65
143
  });
66
144
 
67
- app.post("/api/pairings/:id/reject", async (req, res) => {
68
- const channel = req.body.channel || "telegram";
69
- const result = await clawCmd(`pairing reject ${channel} ${req.params.id}`);
70
- res.json(result);
145
+ app.post("/api/pairings/:id/reject", (req, res) => {
146
+ const channel = String(req.body.channel || "telegram").trim();
147
+ const accountId = String(req.body?.accountId || "").trim();
148
+ try {
149
+ const removed = removeRequestFromPairingStore({
150
+ fsModule,
151
+ openclawDir,
152
+ channel,
153
+ code: req.params.id,
154
+ accountId,
155
+ });
156
+ pairingCache.ts = 0;
157
+ if (removed) {
158
+ console.log(`[alphaclaw] Rejected pairing request ${req.params.id} for ${channel}${accountId ? `/${accountId}` : ""}`);
159
+ return res.json({ ok: true, removed: true });
160
+ }
161
+ return res.status(404).json({
162
+ ok: false,
163
+ removed: false,
164
+ error: "Pairing request not found",
165
+ });
166
+ } catch (error) {
167
+ console.error(`[alphaclaw] Pairing reject error: ${error.message}`);
168
+ res.status(500).json({ ok: false, error: error.message });
169
+ }
71
170
  });
72
171
 
73
- let devicePairingCache = { pending: [], ts: 0 };
172
+ let devicePairingCache = { pending: [], cliAutoApproveComplete: false, ts: 0 };
74
173
  const kDevicePairingCacheTtl = 3000;
75
174
 
76
175
  app.get("/api/devices", async (req, res) => {
77
- if (!isOnboarded()) return res.json({ pending: [] });
176
+ if (!isOnboarded()) {
177
+ return res.json({ pending: [], cliAutoApproveComplete: hasCliAutoApproveMarker() });
178
+ }
78
179
  if (Date.now() - devicePairingCache.ts < kDevicePairingCacheTtl) {
79
- return res.json({ pending: devicePairingCache.pending });
180
+ return res.json({
181
+ pending: devicePairingCache.pending,
182
+ cliAutoApproveComplete: devicePairingCache.cliAutoApproveComplete,
183
+ });
80
184
  }
81
185
  const result = await clawCmd("devices list --json", { quiet: true });
82
- if (!result.ok) return res.json({ pending: [] });
186
+ if (!result.ok) {
187
+ return res.json({ pending: [], cliAutoApproveComplete: hasCliAutoApproveMarker() });
188
+ }
83
189
  try {
84
190
  const parsed = JSON.parse(result.stdout);
85
191
  const pendingList = Array.isArray(parsed.pending) ? parsed.pending : [];
@@ -117,10 +223,11 @@ const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openc
117
223
  scopes: d.scopes || [],
118
224
  ts: d.ts || null,
119
225
  }));
120
- devicePairingCache = { pending, ts: Date.now() };
121
- res.json({ pending });
226
+ const cliAutoApproveComplete = hasCliAutoApproveMarker();
227
+ devicePairingCache = { pending, cliAutoApproveComplete, ts: Date.now() };
228
+ res.json({ pending, cliAutoApproveComplete });
122
229
  } catch {
123
- res.json({ pending: [] });
230
+ res.json({ pending: [], cliAutoApproveComplete: hasCliAutoApproveMarker() });
124
231
  }
125
232
  });
126
233
 
@@ -137,4 +244,7 @@ const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openc
137
244
  });
138
245
  };
139
246
 
140
- module.exports = { registerPairingRoutes };
247
+ module.exports = {
248
+ registerPairingRoutes,
249
+ removeAccountRequestsFromPairingStore,
250
+ };
@@ -1,4 +1,5 @@
1
1
  const { buildManagedPaths } = require("../internal-files-migration");
2
+ const { readOpenclawConfig } = require("../openclaw-config");
2
3
 
3
4
  const registerSystemRoutes = ({
4
5
  app,
@@ -23,6 +24,7 @@ const registerSystemRoutes = ({
23
24
  authProfiles,
24
25
  }) => {
25
26
  let envRestartPending = false;
27
+ const kManagedChannelTokenPattern = /^(TELEGRAM|DISCORD)_BOT_TOKEN(?:_[A-Z0-9_]+)?$/;
26
28
  const kEnvVarsReservedForUserInput = new Set([
27
29
  "GITHUB_WORKSPACE_REPO",
28
30
  "GOG_KEYRING_PASSWORD",
@@ -34,6 +36,8 @@ const registerSystemRoutes = ({
34
36
  const kReservedUserEnvVarKeys = Array.from(
35
37
  new Set([...kSystemVars, ...kEnvVarsReservedForUserInput]),
36
38
  );
39
+ const isManagedChannelTokenKey = (key) =>
40
+ kManagedChannelTokenPattern.test(String(key || "").trim().toUpperCase());
37
41
  const isReservedUserEnvVar = (key) =>
38
42
  kSystemVars.has(key) || kEnvVarsReservedForUserInput.has(key);
39
43
  const kSystemCronPath = "/etc/cron.d/openclaw-hourly-sync";
@@ -69,12 +73,73 @@ const registerSystemRoutes = ({
69
73
  }
70
74
  return null;
71
75
  };
72
- const buildSessionLabel = (sessionRow = {}) => {
76
+ const toTitleWords = (value) =>
77
+ String(value || "")
78
+ .trim()
79
+ .split(/[-_\s]+/)
80
+ .filter(Boolean)
81
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
82
+ .join(" ");
83
+ const hasScopedBindingFields = (match = {}) =>
84
+ !!match.peer ||
85
+ !!match.parentPeer ||
86
+ !!String(match.guildId || "").trim() ||
87
+ !!String(match.teamId || "").trim() ||
88
+ (Array.isArray(match.roles) && match.roles.length > 0);
89
+ const resolveTelegramAccountIdForAgent = ({ config, agentId }) => {
90
+ const normalizedAgentId = String(agentId || "").trim();
91
+ if (!normalizedAgentId) return "default";
92
+ const bindings = Array.isArray(config?.bindings) ? config.bindings : [];
93
+ for (const binding of bindings) {
94
+ if (String(binding?.agentId || "").trim() !== normalizedAgentId) continue;
95
+ const match = binding?.match || {};
96
+ if (String(match.channel || "").trim() !== "telegram") continue;
97
+ if (hasScopedBindingFields(match)) continue;
98
+ return String(match.accountId || "").trim() || "default";
99
+ }
100
+ return normalizedAgentId === "main" ? "default" : "";
101
+ };
102
+ const resolveTelegramChannelNameForAgent = ({ config, agentId }) => {
103
+ const telegramConfig =
104
+ config?.channels?.telegram && typeof config.channels.telegram === "object"
105
+ ? config.channels.telegram
106
+ : {};
107
+ const accountId = resolveTelegramAccountIdForAgent({ config, agentId });
108
+ const hasAccounts =
109
+ telegramConfig.accounts && typeof telegramConfig.accounts === "object";
110
+ if (hasAccounts) {
111
+ const accountConfig =
112
+ accountId && telegramConfig.accounts?.[accountId]
113
+ ? telegramConfig.accounts[accountId]
114
+ : {};
115
+ const accountName = String(accountConfig?.name || "").trim();
116
+ if (accountName) return accountName;
117
+ } else if (accountId === "default") {
118
+ const legacyName = String(telegramConfig?.name || "").trim();
119
+ if (legacyName) return legacyName;
120
+ }
121
+ return "Telegram";
122
+ };
123
+ const getAgentLabelFromSessionKey = (key = "") => {
124
+ const match = String(key || "").match(/^agent:([^:]+):/);
125
+ const agentId = String(match?.[1] || "").trim();
126
+ if (!agentId) return "Agent";
127
+ if (agentId === "main") return "Main Agent";
128
+ return toTitleWords(agentId);
129
+ };
130
+ const buildSessionLabel = (sessionRow = {}, config = {}) => {
73
131
  const key = String(sessionRow?.key || "");
74
- if (key === "agent:main:main") return "Main agent thread";
132
+ const agentLabel = getAgentLabelFromSessionKey(key);
133
+ const agentKeyMatch = key.match(/^agent:([^:]+):/);
134
+ const agentId = String(agentKeyMatch?.[1] || "").trim();
135
+ const telegramChannelName = resolveTelegramChannelNameForAgent({
136
+ config,
137
+ agentId,
138
+ });
139
+ if (key.endsWith(":main")) return `${agentLabel} - Main Thread`;
75
140
  const telegramMatch = key.match(/:telegram:direct:([^:]+)$/);
76
141
  if (telegramMatch) {
77
- return `Telegram ${telegramMatch[1]}`;
142
+ return `${agentLabel} - Telegram DM (${telegramChannelName})`;
78
143
  }
79
144
  const telegramTopicMatch = key.match(
80
145
  /:telegram:group:([^:]+):topic:([^:]+)$/,
@@ -89,13 +154,15 @@ const registerSystemRoutes = ({
89
154
  const topicName = String(
90
155
  groupEntry?.topics?.[topicId]?.name || "",
91
156
  ).trim();
92
- if (groupName && topicName) return `Telegram ${groupName} · ${topicName}`;
93
- if (topicName) return `Telegram Topic ${topicName}`;
94
- return `Telegram Topic ${topicId}`;
157
+ if (groupName && topicName) {
158
+ return `${agentLabel} - ${telegramChannelName} ${groupName} · ${topicName}`;
159
+ }
160
+ if (topicName) return `${agentLabel} - ${telegramChannelName} Topic ${topicName}`;
161
+ return `${agentLabel} - ${telegramChannelName} Topic ${topicId}`;
95
162
  }
96
163
  const directMatch = key.match(/:direct:([^:]+)$/);
97
164
  if (directMatch) {
98
- return `Direct ${directMatch[1]}`;
165
+ return `${agentLabel} - Direct ${directMatch[1]}`;
99
166
  }
100
167
  return key || "Session";
101
168
  };
@@ -144,12 +211,19 @@ const registerSystemRoutes = ({
144
211
  };
145
212
 
146
213
  const listSendableAgentSessions = async () => {
147
- const result = await clawCmd("sessions --json", { quiet: true });
214
+ const result = await clawCmd("sessions --json --all-agents", {
215
+ quiet: true,
216
+ });
148
217
  if (!result.ok) {
149
218
  throw new Error(result.stderr || "Could not load agent sessions");
150
219
  }
151
220
  const payload = parseJsonFromStdout(result.stdout);
152
221
  const sessions = Array.isArray(payload?.sessions) ? payload.sessions : [];
222
+ const config = readOpenclawConfig({
223
+ fsModule: fs,
224
+ openclawDir: OPENCLAW_DIR,
225
+ fallback: {},
226
+ });
153
227
  return sessions
154
228
  .filter((sessionRow) => {
155
229
  const key = String(sessionRow?.key || "").toLowerCase();
@@ -170,7 +244,7 @@ const registerSystemRoutes = ({
170
244
  key,
171
245
  sessionId: String(sessionRow?.sessionId || ""),
172
246
  updatedAt: Number(sessionRow?.updatedAt) || 0,
173
- label: buildSessionLabel(sessionRow),
247
+ label: buildSessionLabel(sessionRow, config),
174
248
  replyChannel: replyTarget.replyChannel,
175
249
  replyTo: replyTarget.replyTo,
176
250
  };
@@ -281,10 +355,15 @@ const registerSystemRoutes = ({
281
355
  });
282
356
  }
283
357
 
284
- const filtered = vars.filter((v) => !isReservedUserEnvVar(v.key));
358
+ const filtered = vars.filter(
359
+ (v) => !isReservedUserEnvVar(v.key) && !isManagedChannelTokenKey(v.key),
360
+ );
285
361
  const existingLockedVars = readEnvFile().filter((v) =>
286
362
  isReservedUserEnvVar(v.key),
287
363
  );
364
+ const existingManagedChannelVars = readEnvFile().filter((v) =>
365
+ isManagedChannelTokenKey(v.key),
366
+ );
288
367
  const hiddenKnownVarKeys = new Set(
289
368
  kKnownVars
290
369
  .filter(
@@ -298,6 +377,7 @@ const registerSystemRoutes = ({
298
377
  const nextEnvVars = [
299
378
  ...filtered,
300
379
  ...existingHiddenKnownVars,
380
+ ...existingManagedChannelVars,
301
381
  ...existingLockedVars,
302
382
  ];
303
383
  syncChannelConfig(nextEnvVars, "remove");