@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
@@ -20,7 +20,6 @@ const GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN || "";
20
20
  const ENV_FILE_PATH = path.join(kRootDir, ".env");
21
21
  const WORKSPACE_DIR = path.join(OPENCLAW_DIR, "workspace");
22
22
  const kOnboardingMarkerPath = path.join(ALPHACLAW_DIR, "onboarded.json");
23
- const kControlUiSkillPath = path.join(OPENCLAW_DIR, "skills", "control-ui", "SKILL.md");
24
23
  const AUTH_PROFILES_PATH = path.join(
25
24
  OPENCLAW_DIR,
26
25
  "agents",
@@ -364,6 +363,8 @@ const SETUP_API_PREFIXES = [
364
363
  "/api/gmail",
365
364
  "/api/watchdog",
366
365
  "/api/usage",
366
+ "/api/agents",
367
+ "/api/channels",
367
368
  ];
368
369
 
369
370
  module.exports = {
@@ -381,7 +382,6 @@ module.exports = {
381
382
  ENV_FILE_PATH,
382
383
  WORKSPACE_DIR,
383
384
  kOnboardingMarkerPath,
384
- kControlUiSkillPath,
385
385
  AUTH_PROFILES_PATH,
386
386
  CODEX_PROFILE_ID,
387
387
  CODEX_OAUTH_CLIENT_ID,
package/lib/server/env.js CHANGED
@@ -64,7 +64,9 @@ const reloadEnv = () => {
64
64
  const startEnvWatcher = () => {
65
65
  try {
66
66
  fs.watchFile(ENV_FILE_PATH, { interval: 2000 }, () => {
67
- console.log(`[alphaclaw] ${ENV_FILE_PATH} changed externally, reloading...`);
67
+ console.log(
68
+ `[alphaclaw] ${ENV_FILE_PATH} changed externally, reloading...`,
69
+ );
68
70
  reloadEnv();
69
71
  });
70
72
  } catch {}
@@ -7,7 +7,6 @@ const {
7
7
  OPENCLAW_DIR,
8
8
  GATEWAY_HOST,
9
9
  kDefaultGatewayPort,
10
- kControlUiSkillPath,
11
10
  kChannelDefs,
12
11
  kOnboardingMarkerPath,
13
12
  kRootDir,
@@ -65,9 +64,13 @@ const writeOnboardingMarker = (reason) => {
65
64
  );
66
65
  };
67
66
 
67
+ // Legacy backfill: older deployments may only have the control-ui skill as
68
+ // proof of onboarding (before the dedicated marker file existed).
69
+ const kLegacyControlUiSkillPath = path.join(OPENCLAW_DIR, "skills", "control-ui", "SKILL.md");
70
+
68
71
  const isOnboarded = () => {
69
72
  if (fs.existsSync(kOnboardingMarkerPath)) return true;
70
- if (fs.existsSync(kControlUiSkillPath)) {
73
+ if (fs.existsSync(kLegacyControlUiSkillPath)) {
71
74
  writeOnboardingMarker("legacy_artifact_backfill");
72
75
  return true;
73
76
  }
@@ -88,6 +91,18 @@ const getGatewayPort = () => {
88
91
 
89
92
  const getGatewayUrl = () => `http://${GATEWAY_HOST}:${getGatewayPort()}`;
90
93
 
94
+ const normalizeChannelAccountId = (value) => String(value || "").trim() || "default";
95
+
96
+ const resolveCredentialPairingAccountId = ({ channel, fileName }) => {
97
+ const prefix = `${String(channel || "").trim()}-`;
98
+ const suffix = "-allowFrom.json";
99
+ if (!String(fileName || "").startsWith(prefix) || !String(fileName || "").endsWith(suffix)) {
100
+ return "";
101
+ }
102
+ const rawAccountId = String(fileName || "").slice(prefix.length, -suffix.length);
103
+ return normalizeChannelAccountId(rawAccountId);
104
+ };
105
+
91
106
  const isGatewayRunning = () =>
92
107
  new Promise((resolve) => {
93
108
  const sock = net.createConnection(getGatewayPort(), GATEWAY_HOST);
@@ -136,17 +151,26 @@ const launchGatewayProcess = () => {
136
151
  stdio: ["pipe", "pipe", "pipe"],
137
152
  });
138
153
  gatewayChild = child;
139
- if (gatewayLaunchHandler) {
140
- try {
141
- gatewayLaunchHandler({
142
- pid: child.pid,
143
- startedAt: Date.now(),
144
- });
145
- } catch (err) {
146
- console.error(`[alphaclaw] Gateway launch handler error: ${err.message}`);
154
+ let didSignalGatewayReady = false;
155
+ child.stdout.on("data", (d) => {
156
+ const text = Buffer.isBuffer(d) ? d.toString("utf8") : String(d ?? "");
157
+ if (
158
+ !didSignalGatewayReady &&
159
+ gatewayLaunchHandler &&
160
+ text.toLowerCase().includes("listening on")
161
+ ) {
162
+ didSignalGatewayReady = true;
163
+ try {
164
+ gatewayLaunchHandler({
165
+ pid: child.pid,
166
+ startedAt: Date.now(),
167
+ });
168
+ } catch (err) {
169
+ console.error(`[alphaclaw] Gateway launch handler error: ${err.message}`);
170
+ }
147
171
  }
148
- }
149
- child.stdout.on("data", (d) => process.stdout.write(`[gateway] ${d}`));
172
+ process.stdout.write(`[gateway] ${d}`);
173
+ });
150
174
  child.stderr.on("data", (d) => {
151
175
  appendStderrTail(d);
152
176
  process.stderr.write(`[gateway] ${d}`);
@@ -206,6 +230,12 @@ const restartGateway = (reloadEnv) => {
206
230
  runGatewayCmd("--force");
207
231
  };
208
232
 
233
+ const restartGatewayLight = (reloadEnv) => {
234
+ reloadEnv();
235
+ markManagedGatewayExitExpected();
236
+ runGatewayCmd("restart");
237
+ };
238
+
209
239
  const attachGatewaySignalHandlers = () => {
210
240
  process.on("SIGTERM", () => {
211
241
  runGatewayCmd("stop");
@@ -324,10 +354,36 @@ const getChannelStatus = () => {
324
354
  const channels = {};
325
355
 
326
356
  for (const ch of ["telegram", "discord"]) {
327
- if (!config.channels?.[ch]?.enabled) continue;
328
- if (!process.env[kChannelDefs[ch].envKey]) continue;
357
+ const channelConfig =
358
+ config.channels?.[ch] && typeof config.channels[ch] === "object"
359
+ ? config.channels[ch]
360
+ : null;
361
+ if (!channelConfig?.enabled) continue;
329
362
 
330
- let paired = 0;
363
+ const rawAccounts =
364
+ channelConfig.accounts && typeof channelConfig.accounts === "object"
365
+ ? channelConfig.accounts
366
+ : {};
367
+ const accountEntries = Object.keys(rawAccounts).length > 0
368
+ ? Object.entries(rawAccounts)
369
+ : [["default", channelConfig]];
370
+ const configuredAccountIds = new Set(
371
+ accountEntries.map(([accountId]) => normalizeChannelAccountId(accountId)),
372
+ );
373
+ const hasConfiguredToken = accountEntries.some(([accountId, accountConfig]) => {
374
+ const normalizedAccountId = normalizeChannelAccountId(accountId);
375
+ const envKey = normalizedAccountId === "default"
376
+ ? kChannelDefs[ch].envKey
377
+ : `${kChannelDefs[ch].envKey}_${normalizedAccountId.replace(/-/g, "_").toUpperCase()}`;
378
+ return !!process.env[envKey]
379
+ || !!accountConfig?.botToken
380
+ || !!accountConfig?.token;
381
+ });
382
+ if (!hasConfiguredToken) continue;
383
+
384
+ const pairedByAccount = new Map(
385
+ Array.from(configuredAccountIds).map((accountId) => [accountId, 0]),
386
+ );
331
387
  try {
332
388
  const files = fs
333
389
  .readdirSync(credDir)
@@ -335,16 +391,43 @@ const getChannelStatus = () => {
335
391
  (f) => f.startsWith(`${ch}-`) && f.endsWith("-allowFrom.json"),
336
392
  );
337
393
  for (const file of files) {
394
+ const accountId = resolveCredentialPairingAccountId({
395
+ channel: ch,
396
+ fileName: file,
397
+ });
398
+ if (!accountId || !configuredAccountIds.has(accountId)) continue;
338
399
  const data = JSON.parse(
339
400
  fs.readFileSync(`${credDir}/${file}`, "utf8"),
340
401
  );
341
- paired += (data.allowFrom || []).length;
402
+ const nextCount =
403
+ Number(pairedByAccount.get(accountId) || 0)
404
+ + (Array.isArray(data.allowFrom) ? data.allowFrom.length : 0);
405
+ pairedByAccount.set(accountId, nextCount);
342
406
  }
343
407
  } catch {}
344
- const inlineAllowFrom = config.channels[ch]?.allowFrom;
345
- if (Array.isArray(inlineAllowFrom)) paired += inlineAllowFrom.length;
346
-
347
- channels[ch] = { status: paired > 0 ? "paired" : "configured", paired };
408
+ for (const [accountId, accountConfig] of accountEntries) {
409
+ const inlineAllowFrom = accountConfig?.allowFrom;
410
+ if (!Array.isArray(inlineAllowFrom)) continue;
411
+ const normalizedAccountId = normalizeChannelAccountId(accountId);
412
+ const nextCount =
413
+ Number(pairedByAccount.get(normalizedAccountId) || 0) + inlineAllowFrom.length;
414
+ pairedByAccount.set(normalizedAccountId, nextCount);
415
+ }
416
+ const accounts = Object.fromEntries(
417
+ Array.from(pairedByAccount.entries()).map(([accountId, paired]) => [
418
+ accountId,
419
+ { status: paired > 0 ? "paired" : "configured", paired },
420
+ ]),
421
+ );
422
+ const paired = Array.from(pairedByAccount.values()).reduce(
423
+ (total, count) => total + Number(count || 0),
424
+ 0,
425
+ );
426
+ channels[ch] = {
427
+ status: paired > 0 ? "paired" : "configured",
428
+ paired,
429
+ accounts,
430
+ };
348
431
  }
349
432
 
350
433
  return channels;
@@ -365,6 +448,7 @@ module.exports = {
365
448
  runGatewayCmd,
366
449
  startGateway,
367
450
  restartGateway,
451
+ restartGatewayLight,
368
452
  attachGatewaySignalHandlers,
369
453
  ensureGatewayProxyConfig,
370
454
  syncChannelConfig,
@@ -124,18 +124,8 @@ const createGmailServeManager = ({
124
124
  stdio: ["ignore", "pipe", "pipe"],
125
125
  });
126
126
 
127
- child.stdout.on("data", (chunk) => {
128
- const line = String(chunk || "").trim();
129
- if (line) {
130
- console.log(`[alphaclaw] gmail watch serve (${email}): ${line}`);
131
- }
132
- });
133
- child.stderr.on("data", (chunk) => {
134
- const line = String(chunk || "").trim();
135
- if (line) {
136
- console.log(`[alphaclaw] gmail watch serve stderr (${email}): ${line}`);
137
- }
138
- });
127
+ child.stdout.on("data", () => {});
128
+ child.stderr.on("data", () => {});
139
129
 
140
130
  const nextEntry = {
141
131
  accountId,
@@ -16,6 +16,7 @@ const {
16
16
  const { createGmailServeManager } = require("./gmail-serve");
17
17
  const { parseJsonObjectFromNoisyOutput, parseJsonSafe } = require("./utils/json");
18
18
  const { createWebhook } = require("./webhooks");
19
+ const { readOpenclawConfig } = require("./openclaw-config");
19
20
  const { quoteShellArg } = require("./utils/shell");
20
21
 
21
22
  const parseExpirationFromOutput = (raw) => {
@@ -65,11 +66,16 @@ const normalizeDestination = (destination = null) => {
65
66
  if (!destination || typeof destination !== "object") return null;
66
67
  const channel = String(destination?.channel || "").trim();
67
68
  const to = String(destination?.to || "").trim();
68
- if (!channel && !to) return null;
69
+ const agentId = String(destination?.agentId || "").trim();
70
+ if (!channel && !to && !agentId) return null;
69
71
  if (!channel || !to) {
70
72
  throw new Error("destination.channel and destination.to are required");
71
73
  }
72
- return { channel, to };
74
+ return {
75
+ channel,
76
+ to,
77
+ ...(agentId ? { agentId } : {}),
78
+ };
73
79
  };
74
80
 
75
81
  const buildGmailTransformSource = (destination = null) => {
@@ -91,6 +97,9 @@ const buildGmailTransformSource = (destination = null) => {
91
97
  ? [
92
98
  ` channel: ${JSON.stringify(normalizedDestination.channel)},`,
93
99
  ` to: ${JSON.stringify(normalizedDestination.to)},`,
100
+ ...(normalizedDestination.agentId
101
+ ? [` agentId: ${JSON.stringify(normalizedDestination.agentId)},`]
102
+ : []),
94
103
  ]
95
104
  : []),
96
105
  " };",
@@ -99,6 +108,18 @@ const buildGmailTransformSource = (destination = null) => {
99
108
  ].join("\n");
100
109
  };
101
110
 
111
+ const hasGmailWebhookMapping = ({ fs, openclawDir }) => {
112
+ const cfg = readOpenclawConfig({
113
+ fsModule: fs,
114
+ openclawDir,
115
+ fallback: {},
116
+ });
117
+ const mappings = Array.isArray(cfg?.hooks?.mappings) ? cfg.hooks.mappings : [];
118
+ return mappings.some(
119
+ (mapping) => String(mapping?.match?.path || "").trim().toLowerCase() === "gmail",
120
+ );
121
+ };
122
+
102
123
  const getGmailTransformAbsolutePath = (constants) =>
103
124
  path.join(constants.OPENCLAW_DIR, "hooks/transforms/gmail/gmail-transform.mjs");
104
125
 
@@ -295,6 +316,7 @@ const createGmailWatchService = ({
295
316
  transform: { module: gmailTransformModulePath },
296
317
  },
297
318
  transformSource: buildGmailTransformSource(destination),
319
+ overwriteTransform: true,
298
320
  });
299
321
  const webhookAfter = fs.readFileSync(configPath, "utf8");
300
322
  if (webhookBefore !== webhookAfter) {
@@ -367,6 +389,10 @@ const createGmailWatchService = ({
367
389
  String(push.token || ""),
368
390
  )}`;
369
391
  const transformExists = fs.existsSync(getGmailTransformAbsolutePath(constants));
392
+ const webhookExists = hasGmailWebhookMapping({
393
+ fs,
394
+ openclawDir: constants.OPENCLAW_DIR,
395
+ });
370
396
  const commands =
371
397
  projectId && push.token
372
398
  ? {
@@ -385,6 +411,7 @@ const createGmailWatchService = ({
385
411
  pushEndpoint,
386
412
  commands,
387
413
  transformExists,
414
+ webhookExists,
388
415
  configured: Boolean(topicPath && push.token && projectId),
389
416
  };
390
417
  };
@@ -22,7 +22,6 @@ const kReplaceableBootstrapPaths = [
22
22
  ".alphaclaw",
23
23
  "gogcli",
24
24
  path.join("workspace", "hooks", "bootstrap"),
25
- path.join("skills", "control-ui"),
26
25
  path.join("skills", "gog-cli"),
27
26
  ];
28
27
 
@@ -17,7 +17,6 @@ const {
17
17
  } = require("./openclaw");
18
18
  const {
19
19
  ensureOpenclawRuntimeArtifacts,
20
- installControlUiSkill,
21
20
  syncBootstrapPromptFiles,
22
21
  } = require("./workspace");
23
22
  const {
@@ -498,11 +497,6 @@ const createOnboardingService = ({
498
497
  authProfiles?.syncConfigAuthReferencesForAgent?.();
499
498
  ensureGatewayProxyConfig(getBaseUrl(req));
500
499
 
501
- installControlUiSkill({
502
- fs,
503
- openclawDir: OPENCLAW_DIR,
504
- baseUrl: getBaseUrl(req),
505
- });
506
500
  installGogCliSkill({ fs, openclawDir: OPENCLAW_DIR });
507
501
 
508
502
  installHourlyGitSyncScript({ fs, openclawDir: OPENCLAW_DIR });
@@ -32,7 +32,17 @@ const isTelegramWorkspaceEnabled = (fs) => {
32
32
  try {
33
33
  const configPath = `${OPENCLAW_DIR}/openclaw.json`;
34
34
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
35
- return Object.keys(cfg.channels?.telegram?.groups || {}).length > 0;
35
+ const telegramConfig = cfg.channels?.telegram || {};
36
+ const topLevelGroupCount = Object.keys(telegramConfig.groups || {}).length;
37
+ if (topLevelGroupCount > 0) return true;
38
+ const accounts =
39
+ telegramConfig.accounts && typeof telegramConfig.accounts === "object"
40
+ ? telegramConfig.accounts
41
+ : {};
42
+ for (const accountConfig of Object.values(accounts)) {
43
+ if (Object.keys(accountConfig?.groups || {}).length > 0) return true;
44
+ }
45
+ return false;
36
46
  } catch {
37
47
  return false;
38
48
  }
@@ -73,59 +83,85 @@ const renderGoogleAccountsMarkdown = (fs) => {
73
83
  }
74
84
  };
75
85
 
86
+ const resolveAllAgentWorkspaces = (fs) => {
87
+ try {
88
+ const configPath = path.join(OPENCLAW_DIR, "openclaw.json");
89
+ const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
90
+ const list = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
91
+ return list
92
+ .map((entry) => {
93
+ const agentId = String(entry.id || "").trim();
94
+ const workspace = String(entry.workspace || "").trim();
95
+ if (!agentId || !workspace) return null;
96
+ return {
97
+ agentId,
98
+ workspace,
99
+ };
100
+ })
101
+ .filter(Boolean);
102
+ } catch {
103
+ return [];
104
+ }
105
+ };
106
+
76
107
  const syncBootstrapPromptFiles = ({ fs, workspaceDir, baseUrl }) => {
77
108
  try {
78
109
  const setupUiUrl = resolveSetupUiUrl(baseUrl);
79
- const bootstrapDir = path.join(workspaceDir, "hooks", "bootstrap");
80
- fs.mkdirSync(bootstrapDir, { recursive: true });
81
-
82
- // AlphaClaw-managed files are always overwritten (even during import)
83
- fs.copyFileSync(
84
- path.join(kSetupDir, "core-prompts", "AGENTS.md"),
85
- path.join(bootstrapDir, "AGENTS.md"),
86
- );
87
110
 
88
111
  const toolsTemplate = fs.readFileSync(
89
112
  path.join(kSetupDir, "core-prompts", "TOOLS.md"),
90
113
  "utf8",
91
114
  );
92
- let toolsContent = toolsTemplate.replace(
93
- /\{\{SETUP_UI_URL\}\}/g,
94
- setupUiUrl,
95
- );
96
-
97
- const topicSection = renderTopicRegistryMarkdown({
98
- includeSyncGuidance: isTelegramWorkspaceEnabled(fs),
99
- });
100
- if (topicSection) {
101
- toolsContent += topicSection;
102
- }
115
+ const includeSyncGuidance = isTelegramWorkspaceEnabled(fs);
103
116
  const googleAccountsSection = renderGoogleAccountsMarkdown(fs);
104
- if (googleAccountsSection) {
105
- toolsContent += googleAccountsSection;
117
+ const buildToolsContent = ({ agentId = "" } = {}) => {
118
+ let toolsContent = toolsTemplate.replace(/\{\{SETUP_UI_URL\}\}/g, setupUiUrl);
119
+ const topicSection = renderTopicRegistryMarkdown({
120
+ includeSyncGuidance,
121
+ agentId,
122
+ });
123
+ if (topicSection) {
124
+ toolsContent += topicSection;
125
+ }
126
+ if (googleAccountsSection) {
127
+ toolsContent += googleAccountsSection;
128
+ }
129
+ return toolsContent;
130
+ };
131
+
132
+ const agentsSourcePath = path.join(kSetupDir, "core-prompts", "AGENTS.md");
133
+
134
+ const writeToWorkspace = (targetDir, toolsContent) => {
135
+ const bootstrapDir = path.join(targetDir, "hooks", "bootstrap");
136
+ fs.mkdirSync(bootstrapDir, { recursive: true });
137
+ fs.copyFileSync(agentsSourcePath, path.join(bootstrapDir, "AGENTS.md"));
138
+ fs.writeFileSync(path.join(bootstrapDir, "TOOLS.md"), toolsContent);
139
+ };
140
+
141
+ writeToWorkspace(workspaceDir, buildToolsContent());
142
+
143
+ const otherWorkspaces = resolveAllAgentWorkspaces(fs).filter(
144
+ (entry) => path.resolve(entry.workspace) !== path.resolve(workspaceDir),
145
+ );
146
+ for (const entry of otherWorkspaces) {
147
+ try {
148
+ writeToWorkspace(
149
+ entry.workspace,
150
+ buildToolsContent({ agentId: entry.agentId }),
151
+ );
152
+ } catch (e) {
153
+ console.error(
154
+ `[onboard] Bootstrap sync skipped for ${entry.workspace}: ${e.message}`,
155
+ );
156
+ }
106
157
  }
107
158
 
108
- fs.writeFileSync(path.join(bootstrapDir, "TOOLS.md"), toolsContent);
109
159
  console.log("[onboard] Bootstrap prompt files synced");
110
160
  } catch (e) {
111
161
  console.error("[onboard] Bootstrap prompt sync error:", e.message);
112
162
  }
113
163
  };
114
164
 
115
- const installControlUiSkill = ({ fs, openclawDir, baseUrl }) => {
116
- try {
117
- const setupUiUrl = resolveSetupUiUrl(baseUrl);
118
- const skillDir = `${openclawDir}/skills/control-ui`;
119
- fs.mkdirSync(skillDir, { recursive: true });
120
- const skillTemplate = fs.readFileSync(path.join(kSetupDir, "skills", "control-ui", "SKILL.md"), "utf8");
121
- const skillContent = skillTemplate.replace(/\{\{BASE_URL\}\}/g, setupUiUrl);
122
- fs.writeFileSync(`${skillDir}/SKILL.md`, skillContent);
123
- console.log(`[onboard] Control UI skill installed (${setupUiUrl})`);
124
- } catch (e) {
125
- console.error("[onboard] Skill install error:", e.message);
126
- }
127
- };
128
-
129
165
  const ensureOpenclawRuntimeArtifacts = ({
130
166
  fs,
131
167
  openclawDir,
@@ -153,6 +189,6 @@ const ensureOpenclawRuntimeArtifacts = ({
153
189
 
154
190
  module.exports = {
155
191
  ensureOpenclawRuntimeArtifacts,
156
- installControlUiSkill,
192
+ resolveSetupUiUrl,
157
193
  syncBootstrapPromptFiles,
158
194
  };
@@ -0,0 +1,23 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const resolveOpenclawConfigPath = ({ openclawDir }) =>
5
+ path.join(openclawDir, "openclaw.json");
6
+
7
+ const readOpenclawConfig = ({
8
+ fsModule = fs,
9
+ openclawDir,
10
+ fallback = {},
11
+ } = {}) => {
12
+ const configPath = resolveOpenclawConfigPath({ openclawDir });
13
+ try {
14
+ return JSON.parse(fsModule.readFileSync(configPath, "utf8"));
15
+ } catch {
16
+ return fallback;
17
+ }
18
+ };
19
+
20
+ module.exports = {
21
+ resolveOpenclawConfigPath,
22
+ readOpenclawConfig,
23
+ };
@@ -0,0 +1,141 @@
1
+ const crypto = require("crypto");
2
+
3
+ const kDefaultTtlMs = 5 * 60 * 1000;
4
+ const kMaxEventsPerOperation = 200;
5
+
6
+ const formatSseEvent = ({ id, event, data }) => {
7
+ const lines = [];
8
+ if (id) lines.push(`id: ${id}`);
9
+ if (event) lines.push(`event: ${event}`);
10
+ const payload = JSON.stringify(data === undefined ? {} : data);
11
+ for (const line of payload.split("\n")) {
12
+ lines.push(`data: ${line}`);
13
+ }
14
+ return `${lines.join("\n")}\n\n`;
15
+ };
16
+
17
+ const createOperationEventsService = ({ ttlMs = kDefaultTtlMs } = {}) => {
18
+ const operations = new Map();
19
+ let sweepTimer = null;
20
+
21
+ const ensureSweeper = () => {
22
+ if (sweepTimer) return;
23
+ sweepTimer = setInterval(() => {
24
+ const now = Date.now();
25
+ for (const [operationId, state] of operations.entries()) {
26
+ if (state.expiresAt <= now && state.subscribers.size === 0) {
27
+ operations.delete(operationId);
28
+ }
29
+ }
30
+ }, 30_000);
31
+ sweepTimer.unref();
32
+ };
33
+
34
+ const getOperation = (operationId) => {
35
+ const normalized = String(operationId || "").trim();
36
+ if (!normalized) return null;
37
+ return operations.get(normalized) || null;
38
+ };
39
+
40
+ const createOperation = ({ type = "operation" } = {}) => {
41
+ const operationId = crypto.randomUUID();
42
+ operations.set(operationId, {
43
+ id: operationId,
44
+ type: String(type || "operation").trim() || "operation",
45
+ createdAt: Date.now(),
46
+ expiresAt: Date.now() + ttlMs,
47
+ status: "pending",
48
+ nextEventId: 1,
49
+ events: [],
50
+ subscribers: new Set(),
51
+ });
52
+ ensureSweeper();
53
+ return { operationId };
54
+ };
55
+
56
+ const publish = (operationId, { event = "message", data = {} } = {}) => {
57
+ const state = getOperation(operationId);
58
+ if (!state) return false;
59
+ const entry = {
60
+ id: String(state.nextEventId++),
61
+ event: String(event || "message").trim() || "message",
62
+ data: data === undefined ? {} : data,
63
+ ts: Date.now(),
64
+ };
65
+ state.events.push(entry);
66
+ if (state.events.length > kMaxEventsPerOperation) {
67
+ state.events = state.events.slice(-kMaxEventsPerOperation);
68
+ }
69
+ for (const res of state.subscribers) {
70
+ try {
71
+ res.write(formatSseEvent(entry));
72
+ } catch {}
73
+ }
74
+ return true;
75
+ };
76
+
77
+ const complete = (operationId, payload = {}) => {
78
+ const state = getOperation(operationId);
79
+ if (!state) return false;
80
+ state.status = "completed";
81
+ state.expiresAt = Date.now() + ttlMs;
82
+ publish(operationId, {
83
+ event: "done",
84
+ data: payload,
85
+ });
86
+ return true;
87
+ };
88
+
89
+ const fail = (operationId, error) => {
90
+ const state = getOperation(operationId);
91
+ if (!state) return false;
92
+ state.status = "failed";
93
+ state.expiresAt = Date.now() + ttlMs;
94
+ publish(operationId, {
95
+ event: "error",
96
+ data: {
97
+ error: String(error?.message || error || "Operation failed"),
98
+ },
99
+ });
100
+ return true;
101
+ };
102
+
103
+ const subscribe = ({ operationId, req, res }) => {
104
+ const state = getOperation(operationId);
105
+ if (!state) {
106
+ return false;
107
+ }
108
+ res.status(200);
109
+ res.setHeader("Content-Type", "text/event-stream");
110
+ res.setHeader("Cache-Control", "no-cache, no-transform");
111
+ res.setHeader("Connection", "keep-alive");
112
+ res.setHeader("X-Accel-Buffering", "no");
113
+ res.flushHeaders?.();
114
+ res.write(": connected\n\n");
115
+ for (const event of state.events) {
116
+ res.write(formatSseEvent(event));
117
+ }
118
+ state.subscribers.add(res);
119
+ const close = () => {
120
+ state.subscribers.delete(res);
121
+ if (state.expiresAt <= Date.now() && state.subscribers.size === 0) {
122
+ operations.delete(state.id);
123
+ }
124
+ };
125
+ req.on("close", close);
126
+ return true;
127
+ };
128
+
129
+ return {
130
+ createOperation,
131
+ publish,
132
+ complete,
133
+ fail,
134
+ subscribe,
135
+ getOperation,
136
+ };
137
+ };
138
+
139
+ module.exports = {
140
+ createOperationEventsService,
141
+ };