@chrysb/alphaclaw 0.6.0-beta.1 → 0.6.0-beta.2

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,211 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+ import {
5
+ canAgentBindAccount,
6
+ getChannelItemSortRank,
7
+ getResolvedAccountStatusInfo,
8
+ isImplicitDefaultAccount,
9
+ resolveChannelAccountLabel,
10
+ } from "./helpers.js";
11
+
12
+ const html = htm.bind(h);
13
+
14
+ export const useChannelItems = ({
15
+ agentId = "",
16
+ agentNameMap = new Map(),
17
+ channelStatus = {},
18
+ configuredChannelMap = new Map(),
19
+ configuredChannels = [],
20
+ defaultAgentId = "",
21
+ isDefaultAgent = false,
22
+ }) => {
23
+ const hasDiscordAccount = useMemo(() => {
24
+ const discordChannel = configuredChannelMap.get("discord");
25
+ return Array.isArray(discordChannel?.accounts) && discordChannel.accounts.length > 0;
26
+ }, [configuredChannelMap]);
27
+
28
+ const [showAssignedElsewhere, setShowAssignedElsewhere] = useState(false);
29
+
30
+ const channelItemData = useMemo(() => {
31
+ const channelOrderMap = new Map(
32
+ configuredChannels.map((entry, index) => [
33
+ String(entry?.channel || "").trim(),
34
+ index,
35
+ ]),
36
+ );
37
+ const accountOrderMap = new Map(
38
+ configuredChannels.flatMap((entry) =>
39
+ (Array.isArray(entry?.accounts) ? entry.accounts : []).map(
40
+ (account, accountIndex) => [
41
+ `${String(entry?.channel || "").trim()}:${String(account?.id || "").trim() || "default"}`,
42
+ accountIndex,
43
+ ],
44
+ ),
45
+ ),
46
+ );
47
+ const channelIds = Array.from(
48
+ new Set([
49
+ ...configuredChannels.map((entry) => String(entry.channel || "").trim()),
50
+ ]),
51
+ ).filter(Boolean);
52
+
53
+ return channelIds
54
+ .flatMap((channelId) => {
55
+ const configuredChannel = configuredChannelMap.get(channelId);
56
+ const statusInfo = channelStatus?.[channelId] || null;
57
+ const accounts = Array.isArray(configuredChannel?.accounts)
58
+ ? configuredChannel.accounts
59
+ : [];
60
+
61
+ if (!configuredChannel && !statusInfo) return [];
62
+
63
+ return accounts.map((account) => {
64
+ const accountId = String(account?.id || "").trim() || "default";
65
+ const boundAgentId = String(account?.boundAgentId || "").trim();
66
+ const accountStatusInfo = getResolvedAccountStatusInfo({
67
+ account,
68
+ statusInfo,
69
+ accountId,
70
+ });
71
+ const isImplicitDefaultOwned =
72
+ isDefaultAgent &&
73
+ isImplicitDefaultAccount({ accountId, boundAgentId });
74
+ const isOwned = boundAgentId === agentId || isImplicitDefaultOwned;
75
+ const isImplicitDefaultElsewhere =
76
+ !isDefaultAgent &&
77
+ isImplicitDefaultAccount({ accountId, boundAgentId });
78
+ const isAvailable = canAgentBindAccount({
79
+ accountId,
80
+ boundAgentId,
81
+ agentId,
82
+ isDefaultAgent,
83
+ });
84
+ const ownerAgentId =
85
+ boundAgentId ||
86
+ (isImplicitDefaultAccount({ accountId, boundAgentId })
87
+ ? defaultAgentId
88
+ : "");
89
+ const ownerAgentName = String(
90
+ agentNameMap.get(ownerAgentId) || ownerAgentId || "",
91
+ ).trim();
92
+ const canNavigateToOwnerAgent =
93
+ !!ownerAgentId && ownerAgentId !== agentId && !!ownerAgentName;
94
+ const canOpenWorkspace =
95
+ channelId === "telegram" &&
96
+ isOwned &&
97
+ accountStatusInfo?.status === "paired";
98
+
99
+ const accountData = {
100
+ id: accountId,
101
+ provider: channelId,
102
+ name: resolveChannelAccountLabel({ channelId, account }),
103
+ rawName: String(account?.name || "").trim(),
104
+ ownerAgentId,
105
+ ownerAgentName,
106
+ boundAgentId,
107
+ isOwned,
108
+ envKey: String(account?.envKey || "").trim(),
109
+ token: String(account?.token || "").trim(),
110
+ isAvailable,
111
+ isBoundElsewhere:
112
+ !isOwned &&
113
+ (!isAvailable || isImplicitDefaultElsewhere || !!ownerAgentId),
114
+ };
115
+
116
+ return {
117
+ id: `${channelId}:${accountId}`,
118
+ channel: channelId,
119
+ accountId,
120
+ channelOrder: Number(channelOrderMap.get(channelId) ?? 9999),
121
+ accountOrder: Number(
122
+ accountOrderMap.get(`${channelId}:${accountId}`) ?? 9999,
123
+ ),
124
+ label: resolveChannelAccountLabel({ channelId, account }),
125
+ isAwaitingPairing: accountStatusInfo?.status !== "paired",
126
+ canOpenWorkspace,
127
+ canNavigateToOwnerAgent,
128
+ ownerAgentId,
129
+ ownerAgentName,
130
+ accountStatusInfo,
131
+ accountData,
132
+ isOwned,
133
+ isAvailable,
134
+ dimmedLabel: accountData.isBoundElsewhere,
135
+ isBoundElsewhere: accountData.isBoundElsewhere,
136
+ };
137
+ });
138
+ })
139
+ .filter(Boolean)
140
+ .sort((a, b) => {
141
+ const rankDiff = getChannelItemSortRank(a) - getChannelItemSortRank(b);
142
+ if (rankDiff !== 0) return rankDiff;
143
+ const channelOrderDiff =
144
+ Number(a?.channelOrder ?? 9999) - Number(b?.channelOrder ?? 9999);
145
+ if (channelOrderDiff !== 0) return channelOrderDiff;
146
+ const accountOrderDiff =
147
+ Number(a?.accountOrder ?? 9999) - Number(b?.accountOrder ?? 9999);
148
+ if (accountOrderDiff !== 0) return accountOrderDiff;
149
+ return String(a?.label || "").localeCompare(String(b?.label || ""));
150
+ });
151
+ }, [
152
+ agentId,
153
+ agentNameMap,
154
+ channelStatus,
155
+ configuredChannelMap,
156
+ configuredChannels,
157
+ defaultAgentId,
158
+ isDefaultAgent,
159
+ ]);
160
+
161
+ const visibleChannelItems = channelItemData.filter(
162
+ (item) => !item?.isBoundElsewhere,
163
+ );
164
+ const assignedElsewhereItems = channelItemData.filter(
165
+ (item) => !!item?.isBoundElsewhere,
166
+ );
167
+
168
+ useEffect(() => {
169
+ if (assignedElsewhereItems.length === 0) {
170
+ setShowAssignedElsewhere(false);
171
+ return;
172
+ }
173
+ if (visibleChannelItems.length === 0) {
174
+ setShowAssignedElsewhere(true);
175
+ }
176
+ }, [agentId, assignedElsewhereItems.length, visibleChannelItems.length]);
177
+
178
+ const mergedChannelItems = useMemo(() => {
179
+ const baseItems = [...visibleChannelItems];
180
+ if (assignedElsewhereItems.length === 0) return baseItems;
181
+ baseItems.push({
182
+ id: "__assigned_elsewhere_toggle",
183
+ label: html`
184
+ <span class="inline-flex items-center gap-1.5">
185
+ <span class=${`arrow inline-block ${showAssignedElsewhere ? "" : "-rotate-90"}`}>▼</span>
186
+ <span>Assigned elsewhere</span>
187
+ </span>
188
+ `,
189
+ labelClassName: "text-xs",
190
+ clickable: true,
191
+ onClick: () => setShowAssignedElsewhere((current) => !current),
192
+ dimmedLabel: true,
193
+ trailing: html`
194
+ <span class="inline-flex items-center gap-1.5 text-gray-500">
195
+ <span class="text-[11px] px-2 py-0.5 rounded-full border border-border">
196
+ ${assignedElsewhereItems.length}
197
+ </span>
198
+ </span>
199
+ `,
200
+ });
201
+ if (showAssignedElsewhere) {
202
+ baseItems.push(...assignedElsewhereItems);
203
+ }
204
+ return baseItems;
205
+ }, [assignedElsewhereItems, showAssignedElsewhere, visibleChannelItems]);
206
+
207
+ return {
208
+ hasDiscordAccount,
209
+ mergedChannelItems,
210
+ };
211
+ };
@@ -40,6 +40,7 @@ export const AgentPairingSection = ({ agent = {} }) => {
40
40
  const [loadingBindings, setLoadingBindings] = useState(true);
41
41
  const [pairingStatusRefreshing, setPairingStatusRefreshing] = useState(false);
42
42
  const pairingRefreshTimerRef = useRef(null);
43
+ const pairingDelayedRefreshTimerRefs = useRef([]);
43
44
  const agentId = String(agent?.id || "").trim();
44
45
  const isDefaultAgent = !!agent?.default;
45
46
 
@@ -82,6 +83,10 @@ export const AgentPairingSection = ({ agent = {} }) => {
82
83
  if (pairingRefreshTimerRef.current) {
83
84
  clearTimeout(pairingRefreshTimerRef.current);
84
85
  }
86
+ for (const timerId of pairingDelayedRefreshTimerRefs.current) {
87
+ clearTimeout(timerId);
88
+ }
89
+ pairingDelayedRefreshTimerRefs.current = [];
85
90
  },
86
91
  [],
87
92
  );
@@ -217,19 +222,27 @@ export const AgentPairingSection = ({ agent = {} }) => {
217
222
  setPairingStatusRefreshing(false);
218
223
  pairingRefreshTimerRef.current = null;
219
224
  }, 2800);
225
+ for (const timerId of pairingDelayedRefreshTimerRefs.current) {
226
+ clearTimeout(timerId);
227
+ }
228
+ pairingDelayedRefreshTimerRefs.current = [];
220
229
  const refresh = () => {
221
230
  pairingsPoll.refresh();
222
231
  loadBindings();
223
232
  announcePairingsChanged(agentId);
224
233
  };
225
234
  refresh();
226
- setTimeout(refresh, 500);
227
- setTimeout(refresh, 2000);
235
+ pairingDelayedRefreshTimerRefs.current.push(setTimeout(refresh, 500));
236
+ pairingDelayedRefreshTimerRefs.current.push(setTimeout(refresh, 2000));
228
237
  }, [agentId, loadBindings, pairingsPoll]);
229
238
 
230
239
  const handleApprove = async (id, channel, accountId = "") => {
231
- await approvePairing(id, channel, accountId);
232
- refreshAfterPairingAction();
240
+ try {
241
+ await approvePairing(id, channel, accountId);
242
+ refreshAfterPairingAction();
243
+ } catch (err) {
244
+ showToast(err.message || "Could not approve pairing", "error");
245
+ }
233
246
  };
234
247
 
235
248
  const handleReject = async (id, channel, accountId = "") => {
@@ -63,9 +63,19 @@ export const CreateChannelModal = ({
63
63
  ? initialProvider
64
64
  : ALL_CHANNELS[0] || "telegram";
65
65
  const providerLabel = getChannelMeta(nextProvider).label || "Channel";
66
+ const nextSelectedChannel =
67
+ existingChannels.find(
68
+ (entry) =>
69
+ String(entry?.channel || "").trim() === String(nextProvider || "").trim(),
70
+ ) || null;
71
+ const nextProviderHasAccounts =
72
+ Array.isArray(nextSelectedChannel?.accounts)
73
+ && nextSelectedChannel.accounts.length > 0;
66
74
  const nextName = isEditMode
67
75
  ? String(account?.name || "").trim() || providerLabel
68
- : providerLabel;
76
+ : nextProviderHasAccounts
77
+ ? ""
78
+ : providerLabel;
69
79
  const nextAgentId = isEditMode
70
80
  ? String(account?.ownerAgentId || "").trim()
71
81
  || String(initialAgentId || "").trim()
@@ -85,12 +95,16 @@ export const CreateChannelModal = ({
85
95
  setAgentId(nextAgentId);
86
96
  setError("");
87
97
  setNameEditedManually(isEditMode);
88
- }, [visible, initialAgentId, initialProvider, agents, isEditMode, account]);
98
+ }, [
99
+ visible,
100
+ initialAgentId,
101
+ initialProvider,
102
+ agents,
103
+ existingChannels,
104
+ isEditMode,
105
+ account,
106
+ ]);
89
107
 
90
- useEffect(() => {
91
- if (nameEditedManually) return;
92
- setName(getChannelMeta(provider).label || "Channel");
93
- }, [provider, nameEditedManually]);
94
108
  const selectedChannel = useMemo(
95
109
  () =>
96
110
  existingChannels.find(
@@ -103,6 +117,15 @@ export const CreateChannelModal = ({
103
117
  () => Array.isArray(selectedChannel?.accounts) && selectedChannel.accounts.length > 0,
104
118
  [selectedChannel],
105
119
  );
120
+ useEffect(() => {
121
+ if (nameEditedManually) return;
122
+ const providerLabel = getChannelMeta(provider).label || "Channel";
123
+ if (!isEditMode && providerHasAccounts) {
124
+ setName("");
125
+ return;
126
+ }
127
+ setName(providerLabel);
128
+ }, [provider, providerHasAccounts, nameEditedManually, isEditMode]);
106
129
  const isSingleAccountProvider = String(provider || "").trim() === "discord";
107
130
 
108
131
  const accountId = useMemo(() => {
@@ -16,6 +16,10 @@ import {
16
16
  fetchChannelAccounts,
17
17
  updateChannelAccount,
18
18
  } from "../lib/api.js";
19
+ import {
20
+ isImplicitDefaultAccount,
21
+ resolveChannelAccountLabel,
22
+ } from "../lib/channel-accounts.js";
19
23
  import { createChannelAccountWithProgress } from "../lib/channel-create-operation.js";
20
24
  import { CreateChannelModal } from "./agents-tab/create-channel-modal.js";
21
25
  import { showToast } from "./toast.js";
@@ -40,18 +44,6 @@ const getChannelMeta = (channelId = "") => {
40
44
  );
41
45
  };
42
46
 
43
- const resolveChannelAccountLabel = ({ channelId, account = {} }) => {
44
- const providerLabel = getChannelMeta(channelId).label || "Channel";
45
- const configuredName = String(account?.name || "").trim();
46
- if (configuredName) return configuredName;
47
- const accountId = String(account?.id || "").trim();
48
- if (!accountId || accountId === "default") return providerLabel;
49
- return `${providerLabel} ${accountId}`;
50
- };
51
-
52
- const isImplicitDefaultAccount = ({ accountId, boundAgentId }) =>
53
- String(accountId || "").trim() === "default" &&
54
- !String(boundAgentId || "").trim();
55
47
  const announceRestartRequired = () =>
56
48
  window.dispatchEvent(new CustomEvent("alphaclaw:restart-required"));
57
49
 
@@ -60,6 +52,7 @@ export const ChannelsCard = ({
60
52
  items = [],
61
53
  loadingLabel = "Loading...",
62
54
  actions = null,
55
+ renderItem = null,
63
56
  }) => html`
64
57
  <div class="bg-surface border border-border rounded-xl p-4">
65
58
  <div class="flex items-center justify-between gap-3 mb-3">
@@ -71,6 +64,10 @@ export const ChannelsCard = ({
71
64
  ? items.map((item) => {
72
65
  const channelMeta = getChannelMeta(item.channel || item.id);
73
66
  const clickable = !!item.clickable;
67
+ const customItem = renderItem
68
+ ? renderItem({ item, channelMeta, clickable })
69
+ : null;
70
+ if (customItem) return customItem;
74
71
  return html`
75
72
  <div
76
73
  key=${item.id || item.channel}
@@ -342,7 +339,11 @@ export const Channels = ({
342
339
  const accountData = {
343
340
  id: accountId,
344
341
  provider: channelId,
345
- name: resolveChannelAccountLabel({ channelId, account }),
342
+ name: resolveChannelAccountLabel({
343
+ channelId,
344
+ account,
345
+ providerLabel: getChannelMeta(channelId).label || "Channel",
346
+ }),
346
347
  ownerAgentId,
347
348
  envKey: String(account?.envKey || "").trim(),
348
349
  token: String(account?.token || "").trim(),
@@ -422,7 +423,11 @@ export const Channels = ({
422
423
  accountOrder: Number(
423
424
  accountOrderMap.get(`${channelId}:${accountId}`) ?? 9999,
424
425
  ),
425
- label: resolveChannelAccountLabel({ channelId, account }),
426
+ label: resolveChannelAccountLabel({
427
+ channelId,
428
+ account,
429
+ providerLabel: getChannelMeta(channelId).label || "Channel",
430
+ }),
426
431
  isAwaitingPairing: accountStatus !== "paired",
427
432
  detailText: isClickable ? "Workspace" : "",
428
433
  detailChevron: isClickable,
@@ -0,0 +1,20 @@
1
+ export const resolveChannelAccountLabel = ({
2
+ channelId,
3
+ account = {},
4
+ providerLabel = "",
5
+ }) => {
6
+ const fallbackProviderLabel = channelId
7
+ ? channelId.charAt(0).toUpperCase() + channelId.slice(1)
8
+ : "Channel";
9
+ const resolvedProviderLabel = String(providerLabel || "").trim()
10
+ || fallbackProviderLabel;
11
+ const configuredName = String(account?.name || "").trim();
12
+ if (configuredName) return configuredName;
13
+ const accountId = String(account?.id || "").trim();
14
+ if (!accountId || accountId === "default") return resolvedProviderLabel;
15
+ return `${resolvedProviderLabel} ${accountId}`;
16
+ };
17
+
18
+ export const isImplicitDefaultAccount = ({ accountId, boundAgentId }) =>
19
+ String(accountId || "").trim() === "default" &&
20
+ !String(boundAgentId || "").trim();
@@ -0,0 +1,207 @@
1
+ const path = require("path");
2
+
3
+ const {
4
+ kDefaultAgentId,
5
+ resolveAgentWorkspacePath,
6
+ loadConfig,
7
+ saveConfig,
8
+ cloneJson,
9
+ getSafeStat,
10
+ calculatePathSizeBytes,
11
+ withNormalizedAgentsConfig,
12
+ isValidAgentId,
13
+ resolveRequestedWorkspacePath,
14
+ ensureAgentScaffold,
15
+ } = require("./shared");
16
+
17
+ const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
18
+ const listAgents = () => {
19
+ const cfg = withNormalizedAgentsConfig({
20
+ OPENCLAW_DIR,
21
+ cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
22
+ });
23
+ return (cfg.agents?.list || []).map((entry) => ({
24
+ ...entry,
25
+ id: String(entry.id || "").trim(),
26
+ name: String(entry.name || "").trim() || String(entry.id || "").trim(),
27
+ default: !!entry.default,
28
+ }));
29
+ };
30
+
31
+ const getAgent = (agentId) => {
32
+ const normalized = String(agentId || "").trim();
33
+ return listAgents().find((entry) => entry.id === normalized) || null;
34
+ };
35
+
36
+ const getAgentWorkspaceSize = (agentId) => {
37
+ const normalized = String(agentId || "").trim();
38
+ const agent = getAgent(normalized);
39
+ if (!agent) throw new Error(`Agent "${normalized}" not found`);
40
+ const workspacePath = String(
41
+ agent.workspace ||
42
+ resolveAgentWorkspacePath({ OPENCLAW_DIR, agentId: normalized }),
43
+ ).trim();
44
+ if (!workspacePath) {
45
+ return { workspacePath: "", exists: false, sizeBytes: 0 };
46
+ }
47
+ const stat = getSafeStat({ fsImpl, targetPath: workspacePath });
48
+ if (!stat) {
49
+ return { workspacePath, exists: false, sizeBytes: 0 };
50
+ }
51
+ return {
52
+ workspacePath,
53
+ exists: true,
54
+ sizeBytes: calculatePathSizeBytes({ fsImpl, targetPath: workspacePath }),
55
+ };
56
+ };
57
+
58
+ const createAgent = (input = {}) => {
59
+ const agentId = String(input.id || "").trim();
60
+ if (!isValidAgentId(agentId)) {
61
+ throw new Error(
62
+ "Agent id must be lowercase letters, numbers, and hyphens only",
63
+ );
64
+ }
65
+
66
+ const cfg = withNormalizedAgentsConfig({
67
+ OPENCLAW_DIR,
68
+ cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
69
+ });
70
+ const existing = cfg.agents.list.find((entry) => entry.id === agentId);
71
+ if (existing) {
72
+ throw new Error(`Agent "${agentId}" already exists`);
73
+ }
74
+
75
+ const workspacePath = resolveRequestedWorkspacePath({
76
+ OPENCLAW_DIR,
77
+ agentId,
78
+ workspaceFolder: input.workspaceFolder,
79
+ });
80
+ const { workspacePath: scaffoldWorkspacePath, agentDirPath } =
81
+ ensureAgentScaffold({
82
+ fsImpl,
83
+ workspacePath,
84
+ OPENCLAW_DIR,
85
+ agentId,
86
+ });
87
+ const nextAgent = {
88
+ id: agentId,
89
+ name: String(input.name || "").trim() || agentId,
90
+ default: false,
91
+ workspace: scaffoldWorkspacePath,
92
+ agentDir: agentDirPath,
93
+ ...(input.model ? { model: input.model } : {}),
94
+ ...(input.identity && typeof input.identity === "object"
95
+ ? { identity: { ...input.identity } }
96
+ : {}),
97
+ };
98
+ cfg.agents.list = [...cfg.agents.list, nextAgent];
99
+ saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
100
+ return nextAgent;
101
+ };
102
+
103
+ const updateAgent = (agentId, patch = {}) => {
104
+ const normalized = String(agentId || "").trim();
105
+ const cfg = withNormalizedAgentsConfig({
106
+ OPENCLAW_DIR,
107
+ cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
108
+ });
109
+ const index = cfg.agents.list.findIndex((entry) => entry.id === normalized);
110
+ if (index < 0) throw new Error(`Agent "${normalized}" not found`);
111
+ const current = cfg.agents.list[index];
112
+ const next = {
113
+ ...current,
114
+ ...(patch.name !== undefined
115
+ ? { name: String(patch.name || "").trim() }
116
+ : {}),
117
+ ...(patch.identity !== undefined
118
+ ? {
119
+ identity:
120
+ patch.identity && typeof patch.identity === "object"
121
+ ? { ...patch.identity }
122
+ : {},
123
+ }
124
+ : {}),
125
+ };
126
+ if (patch.model !== undefined) {
127
+ if (patch.model === null) {
128
+ delete next.model;
129
+ } else {
130
+ next.model = patch.model;
131
+ }
132
+ }
133
+ if (!String(next.name || "").trim()) next.name = normalized;
134
+ cfg.agents.list[index] = next;
135
+ saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
136
+ return next;
137
+ };
138
+
139
+ const setDefaultAgent = (agentId) => {
140
+ const normalized = String(agentId || "").trim();
141
+ const cfg = withNormalizedAgentsConfig({
142
+ OPENCLAW_DIR,
143
+ cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
144
+ });
145
+ const exists = cfg.agents.list.some((entry) => entry.id === normalized);
146
+ if (!exists) throw new Error(`Agent "${normalized}" not found`);
147
+ cfg.agents.list = cfg.agents.list.map((entry) => ({
148
+ ...entry,
149
+ default: entry.id === normalized,
150
+ }));
151
+ saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
152
+ return cfg.agents.list.find((entry) => entry.id === normalized) || null;
153
+ };
154
+
155
+ const deleteAgent = (agentId, { keepWorkspace = true } = {}) => {
156
+ const normalized = String(agentId || "").trim();
157
+ if (!normalized || normalized === kDefaultAgentId) {
158
+ throw new Error("The default main agent cannot be deleted");
159
+ }
160
+ const cfg = withNormalizedAgentsConfig({
161
+ OPENCLAW_DIR,
162
+ cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
163
+ });
164
+ const target = cfg.agents.list.find((entry) => entry.id === normalized);
165
+ if (!target) throw new Error(`Agent "${normalized}" not found`);
166
+ if (target.default) {
167
+ throw new Error("Default agent cannot be deleted");
168
+ }
169
+ cfg.agents.list = cfg.agents.list.filter(
170
+ (entry) => entry.id !== normalized,
171
+ );
172
+ if (Array.isArray(cfg.bindings)) {
173
+ cfg.bindings = cfg.bindings.filter(
174
+ (binding) => String(binding?.agentId || "") !== normalized,
175
+ );
176
+ }
177
+ saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
178
+
179
+ if (!keepWorkspace) {
180
+ const workspacePath = String(
181
+ target.workspace ||
182
+ resolveAgentWorkspacePath({
183
+ OPENCLAW_DIR,
184
+ agentId: normalized,
185
+ }),
186
+ ).trim();
187
+ const agentDirPath = path.join(OPENCLAW_DIR, "agents", normalized);
188
+ if (workspacePath) {
189
+ fsImpl.rmSync(workspacePath, { recursive: true, force: true });
190
+ }
191
+ fsImpl.rmSync(agentDirPath, { recursive: true, force: true });
192
+ }
193
+ return { ok: true };
194
+ };
195
+
196
+ return {
197
+ listAgents,
198
+ getAgent,
199
+ getAgentWorkspaceSize,
200
+ createAgent,
201
+ updateAgent,
202
+ setDefaultAgent,
203
+ deleteAgent,
204
+ };
205
+ };
206
+
207
+ module.exports = { createAgentsDomain };
@@ -0,0 +1,74 @@
1
+ const {
2
+ loadConfig,
3
+ saveConfig,
4
+ cloneJson,
5
+ normalizeBindingMatch,
6
+ matchesBinding,
7
+ appendBindingToConfig,
8
+ withNormalizedAgentsConfig,
9
+ } = require("./shared");
10
+
11
+ const createBindingsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
12
+ const getBindingsForAgent = (agentId) => {
13
+ const normalized = String(agentId || "").trim();
14
+ const cfg = withNormalizedAgentsConfig({
15
+ OPENCLAW_DIR,
16
+ cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
17
+ });
18
+ const bindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
19
+ return bindings
20
+ .filter((binding) => String(binding?.agentId || "").trim() === normalized)
21
+ .map((binding) => cloneJson(binding));
22
+ };
23
+
24
+ const addBinding = (agentId, input = {}) => {
25
+ const normalizedAgentId = String(agentId || "").trim();
26
+ const cfg = withNormalizedAgentsConfig({
27
+ OPENCLAW_DIR,
28
+ cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
29
+ });
30
+ const agent = cfg.agents.list.find(
31
+ (entry) => entry.id === normalizedAgentId,
32
+ );
33
+ if (!agent) throw new Error(`Agent "${normalizedAgentId}" not found`);
34
+ const match = normalizeBindingMatch(input);
35
+ const nextBinding = appendBindingToConfig({
36
+ cfg,
37
+ agentId: normalizedAgentId,
38
+ match,
39
+ });
40
+ saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
41
+ return nextBinding;
42
+ };
43
+
44
+ const removeBinding = (agentId, input = {}) => {
45
+ const normalizedAgentId = String(agentId || "").trim();
46
+ const cfg = withNormalizedAgentsConfig({
47
+ OPENCLAW_DIR,
48
+ cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
49
+ });
50
+ const bindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
51
+ const nextMatch = normalizeBindingMatch(input);
52
+ const nextBindings = bindings.filter(
53
+ (binding) =>
54
+ !(
55
+ String(binding?.agentId || "").trim() === normalizedAgentId &&
56
+ matchesBinding(binding?.match || {}, nextMatch)
57
+ ),
58
+ );
59
+ if (nextBindings.length === bindings.length) {
60
+ throw new Error("Binding not found");
61
+ }
62
+ cfg.bindings = nextBindings;
63
+ saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
64
+ return { ok: true };
65
+ };
66
+
67
+ return {
68
+ getBindingsForAgent,
69
+ addBinding,
70
+ removeBinding,
71
+ };
72
+ };
73
+
74
+ module.exports = { createBindingsDomain };