@chrysb/alphaclaw 0.6.1 → 0.6.2-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 (50) hide show
  1. package/lib/public/css/agents.css +1 -1
  2. package/lib/public/css/cron.css +535 -0
  3. package/lib/public/css/theme.css +72 -0
  4. package/lib/public/js/app.js +45 -10
  5. package/lib/public/js/components/action-button.js +26 -20
  6. package/lib/public/js/components/agents-tab/agent-detail-panel.js +98 -17
  7. package/lib/public/js/components/agents-tab/agent-tools/index.js +105 -0
  8. package/lib/public/js/components/agents-tab/agent-tools/tool-catalog.js +289 -0
  9. package/lib/public/js/components/agents-tab/agent-tools/use-agent-tools.js +128 -0
  10. package/lib/public/js/components/agents-tab/index.js +4 -0
  11. package/lib/public/js/components/cron-tab/cron-calendar-helpers.js +385 -0
  12. package/lib/public/js/components/cron-tab/cron-calendar.js +441 -0
  13. package/lib/public/js/components/cron-tab/cron-helpers.js +326 -0
  14. package/lib/public/js/components/cron-tab/cron-job-detail.js +425 -0
  15. package/lib/public/js/components/cron-tab/cron-job-list.js +305 -0
  16. package/lib/public/js/components/cron-tab/cron-job-usage.js +70 -0
  17. package/lib/public/js/components/cron-tab/cron-overview.js +599 -0
  18. package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +277 -0
  19. package/lib/public/js/components/cron-tab/index.js +100 -0
  20. package/lib/public/js/components/cron-tab/use-cron-tab.js +366 -0
  21. package/lib/public/js/components/doctor/summary-cards.js +5 -11
  22. package/lib/public/js/components/icons.js +13 -0
  23. package/lib/public/js/components/pill-tabs.js +33 -0
  24. package/lib/public/js/components/pop-actions.js +58 -0
  25. package/lib/public/js/components/routes/agents-route.js +4 -0
  26. package/lib/public/js/components/routes/cron-route.js +9 -0
  27. package/lib/public/js/components/routes/index.js +1 -0
  28. package/lib/public/js/components/segmented-control.js +15 -9
  29. package/lib/public/js/components/summary-stat-card.js +17 -0
  30. package/lib/public/js/components/tooltip.js +50 -4
  31. package/lib/public/js/components/watchdog-tab.js +46 -1
  32. package/lib/public/js/lib/api.js +94 -0
  33. package/lib/public/js/lib/app-navigation.js +2 -0
  34. package/lib/public/js/lib/storage-keys.js +1 -0
  35. package/lib/public/setup.html +1 -0
  36. package/lib/server/agents/agents.js +15 -0
  37. package/lib/server/constants.js +1 -0
  38. package/lib/server/cost-utils.js +312 -0
  39. package/lib/server/cron-service.js +461 -0
  40. package/lib/server/db/usage/index.js +100 -1
  41. package/lib/server/db/usage/pricing.js +1 -83
  42. package/lib/server/db/usage/sessions.js +4 -1
  43. package/lib/server/db/usage/shared.js +2 -1
  44. package/lib/server/db/usage/summary.js +5 -1
  45. package/lib/server/onboarding/index.js +39 -5
  46. package/lib/server/onboarding/openclaw.js +25 -19
  47. package/lib/server/onboarding/validation.js +28 -0
  48. package/lib/server/routes/cron.js +148 -0
  49. package/lib/server.js +13 -0
  50. package/package.json +1 -1
@@ -72,19 +72,21 @@ export const ActionButton = ({
72
72
  : "opacity-80"
73
73
  }`
74
74
  : "";
75
- const spinnerSizeClass = size === "md" || size === "lg" ? "h-4 w-4" : "h-3 w-3";
75
+ const spinnerSizeClass =
76
+ size === "md" || size === "lg" ? "h-4 w-4" : "h-3 w-3";
76
77
  const isInlineLoading = loadingMode === "inline";
77
78
  const IdleIcon = idleIcon;
78
- const idleContent = iconOnly && IdleIcon
79
- ? html`<${IdleIcon} className=${idleIconClassName} />`
80
- : IdleIcon
81
- ? html`
82
- <span class="inline-flex items-center gap-1.5 leading-none">
83
- <${IdleIcon} className=${idleIconClassName} />
84
- ${idleLabel}
85
- </span>
86
- `
87
- : idleLabel;
79
+ const idleContent =
80
+ iconOnly && IdleIcon
81
+ ? html`<${IdleIcon} className=${idleIconClassName} />`
82
+ : IdleIcon
83
+ ? html`
84
+ <span class="inline-flex items-center gap-1.5 leading-none">
85
+ <${IdleIcon} className=${idleIconClassName} />
86
+ ${idleLabel}
87
+ </span>
88
+ `
89
+ : idleLabel;
88
90
  const currentLabel = loading && !isInlineLoading ? loadingLabel : idleContent;
89
91
 
90
92
  return html`
@@ -98,11 +100,15 @@ export const ActionButton = ({
98
100
  >
99
101
  ${isInlineLoading
100
102
  ? html`
101
- <span class="relative inline-flex items-center justify-center leading-none">
103
+ <span
104
+ class="relative inline-flex items-center justify-center leading-none"
105
+ >
102
106
  <span class=${loading ? "invisible" : ""}>${currentLabel}</span>
103
107
  ${loading
104
108
  ? html`
105
- <span class="absolute inset-0 inline-flex items-center justify-center">
109
+ <span
110
+ class="absolute inset-0 inline-flex items-center justify-center"
111
+ >
106
112
  <${LoadingSpinner} className=${spinnerSizeClass} />
107
113
  </span>
108
114
  `
@@ -110,13 +116,13 @@ export const ActionButton = ({
110
116
  </span>
111
117
  `
112
118
  : loading
113
- ? html`
114
- <span class="inline-flex items-center gap-1.5 leading-none">
115
- <${LoadingSpinner} className=${spinnerSizeClass} />
116
- ${currentLabel}
117
- </span>
118
- `
119
- : currentLabel}
119
+ ? html`
120
+ <span class="inline-flex items-center gap-1.5 leading-none">
121
+ <${LoadingSpinner} className=${spinnerSizeClass} />
122
+ ${currentLabel}
123
+ </span>
124
+ `
125
+ : currentLabel}
120
126
  </button>
121
127
  `;
122
128
  };
@@ -1,22 +1,69 @@
1
1
  import { h } from "https://esm.sh/preact";
2
+ import { useState, useCallback } from "https://esm.sh/preact/hooks";
2
3
  import htm from "https://esm.sh/htm";
3
4
  import { ActionButton } from "../action-button.js";
4
5
  import { Badge } from "../badge.js";
6
+ import { PillTabs } from "../pill-tabs.js";
7
+ import { PopActions } from "../pop-actions.js";
5
8
  import { AgentOverview } from "./agent-overview/index.js";
9
+ import { AgentToolsPanel } from "./agent-tools/index.js";
10
+ import { useAgentTools } from "./agent-tools/use-agent-tools.js";
6
11
 
7
12
  const html = htm.bind(h);
8
13
 
14
+ const kDetailTabs = [
15
+ { label: "Overview", value: "overview" },
16
+ { label: "Tools", value: "tools" },
17
+ ];
18
+
19
+ const PencilIcon = ({ className = "w-3.5 h-3.5" }) => html`
20
+ <svg
21
+ xmlns="http://www.w3.org/2000/svg"
22
+ viewBox="0 0 24 24"
23
+ fill="currentColor"
24
+ class=${className}
25
+ >
26
+ <path
27
+ d="M15.7279 9.57627L14.3137 8.16206L5 17.4758V18.89H6.41421L15.7279 9.57627ZM17.1421 8.16206L18.5563 6.74785L17.1421 5.33363L15.7279 6.74785L17.1421 8.16206ZM7.24264 20.89H3V16.6473L16.435 3.21231C16.8256 2.82179 17.4587 2.82179 17.8492 3.21231L20.6777 6.04074C21.0682 6.43126 21.0682 7.06443 20.6777 7.45495L7.24264 20.89Z"
28
+ />
29
+ </svg>
30
+ `;
31
+
9
32
  export const AgentDetailPanel = ({
10
33
  agent = null,
11
34
  agents = [],
35
+ activeTab = "overview",
12
36
  saving = false,
13
37
  onUpdateAgent = async () => {},
14
38
  onSetLocation = () => {},
39
+ onSelectTab = () => {},
15
40
  onEdit = () => {},
16
41
  onDelete = () => {},
17
42
  onSetDefault = () => {},
18
43
  onOpenWorkspace = () => {},
19
44
  }) => {
45
+ const tools = useAgentTools({ agent: agent || {} });
46
+ const [savingTools, setSavingTools] = useState(false);
47
+
48
+ const handleSaveTools = useCallback(async () => {
49
+ if (!agent) return;
50
+ setSavingTools(true);
51
+ try {
52
+ const nextAgent = await onUpdateAgent(
53
+ agent.id,
54
+ { tools: tools.toolsConfig },
55
+ "Tool access updated",
56
+ );
57
+ tools.markSaved(nextAgent?.tools || tools.toolsConfig);
58
+ } catch {
59
+ // toast handled by parent
60
+ } finally {
61
+ setSavingTools(false);
62
+ }
63
+ }, [agent, tools.toolsConfig, tools.markSaved, onUpdateAgent]);
64
+
65
+ const isSaving = saving || savingTools;
66
+
20
67
  if (!agent) {
21
68
  return html`
22
69
  <div class="agents-detail-panel">
@@ -32,10 +79,18 @@ export const AgentDetailPanel = ({
32
79
  <div class="agents-detail-inner">
33
80
  <div class="agents-detail-header">
34
81
  <div class="min-w-0">
35
- <div class="flex items-center gap-3 min-w-0">
82
+ <div class="flex items-center gap-2 min-w-0">
36
83
  <span class="agents-detail-header-title">
37
84
  ${agent.name || agent.id}
38
85
  </span>
86
+ <button
87
+ type="button"
88
+ class="text-gray-500 hover:text-gray-300 transition-colors p-0.5 -ml-0.5"
89
+ onclick=${() => onEdit(agent)}
90
+ title="Edit agent name"
91
+ >
92
+ <${PencilIcon} />
93
+ </button>
39
94
  ${agent.default
40
95
  ? html`<${Badge} tone="cyan">Default</${Badge}>`
41
96
  : null}
@@ -44,29 +99,55 @@ export const AgentDetailPanel = ({
44
99
  <span class="font-mono">${agent.id}</span>
45
100
  </div>
46
101
  </div>
47
- <div class="flex items-start gap-2 shrink-0">
102
+ <${PopActions} visible=${tools.dirty}>
48
103
  <${ActionButton}
49
- onClick=${() => onEdit(agent)}
50
- disabled=${saving}
104
+ onClick=${tools.reset}
105
+ disabled=${isSaving}
51
106
  tone="secondary"
52
107
  size="sm"
53
- idleLabel="Edit"
108
+ idleLabel="Cancel"
54
109
  className="text-xs"
55
110
  />
56
- </div>
111
+ <${ActionButton}
112
+ onClick=${handleSaveTools}
113
+ disabled=${isSaving}
114
+ loading=${isSaving}
115
+ loadingMode="inline"
116
+ tone="primary"
117
+ size="sm"
118
+ idleLabel="Save changes"
119
+ loadingLabel="Saving…"
120
+ className="text-xs"
121
+ />
122
+ </${PopActions}>
57
123
  </div>
124
+ <${PillTabs}
125
+ tabs=${kDetailTabs}
126
+ activeTab=${activeTab}
127
+ onSelectTab=${onSelectTab}
128
+ className="flex items-center gap-2 pt-6"
129
+ />
58
130
  <div class="agents-detail-content">
59
- <${AgentOverview}
60
- agent=${agent}
61
- agents=${agents}
62
- saving=${saving}
63
- onUpdateAgent=${onUpdateAgent}
64
- onSetLocation=${onSetLocation}
65
- onOpenWorkspace=${onOpenWorkspace}
66
- onSwitchToModels=${() => onSetLocation("/models")}
67
- onSetDefault=${onSetDefault}
68
- onDelete=${onDelete}
69
- />
131
+ ${activeTab === "overview"
132
+ ? html`
133
+ <${AgentOverview}
134
+ agent=${agent}
135
+ agents=${agents}
136
+ saving=${saving}
137
+ onUpdateAgent=${onUpdateAgent}
138
+ onSetLocation=${onSetLocation}
139
+ onOpenWorkspace=${onOpenWorkspace}
140
+ onSwitchToModels=${() => onSetLocation("/models")}
141
+ onSetDefault=${onSetDefault}
142
+ onDelete=${onDelete}
143
+ />
144
+ `
145
+ : html`
146
+ <${AgentToolsPanel}
147
+ agent=${agent}
148
+ tools=${tools}
149
+ />
150
+ `}
70
151
  </div>
71
152
  </div>
72
153
  </div>
@@ -0,0 +1,105 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { ToggleSwitch } from "../../toggle-switch.js";
4
+ import { InfoTooltip } from "../../info-tooltip.js";
5
+ import { SegmentedControl } from "../../segmented-control.js";
6
+ import { kSections, kToolProfiles, kProfileLabels } from "./tool-catalog.js";
7
+
8
+ const html = htm.bind(h);
9
+
10
+ const kProfileDescriptions = {
11
+ minimal: "Only session status — grant specific tools with alsoAllow",
12
+ messaging: "Session access and messaging — ideal for notification agents",
13
+ coding: "File I/O, shell, memory, sessions, cron, and image generation",
14
+ full: "All tools enabled, no restrictions",
15
+ };
16
+
17
+ const kProfileOptions = kToolProfiles.map((p) => ({
18
+ label: kProfileLabels[p],
19
+ value: p,
20
+ title: kProfileDescriptions[p],
21
+ }));
22
+
23
+ const ToolRow = ({ tool, onToggle }) => html`
24
+ <div class="flex items-center justify-between gap-3 py-2.5 px-4">
25
+ <div class="min-w-0">
26
+ <div class="text-sm text-gray-200 flex items-center gap-1.5">
27
+ <span>${tool.label}</span>
28
+ ${tool.help
29
+ ? html`<${InfoTooltip} text=${tool.help} widthClass="w-72" />`
30
+ : null}
31
+ </div>
32
+ <span class="text-xs font-mono text-gray-500">${tool.id}</span>
33
+ </div>
34
+ <${ToggleSwitch}
35
+ checked=${tool.enabled}
36
+ onChange=${(checked) => onToggle(tool.id, checked)}
37
+ label=${null}
38
+ />
39
+ </div>
40
+ `;
41
+
42
+ const ToolSection = ({ section, toolStates, onToggle }) => {
43
+ const sectionTools = toolStates.filter((t) => t.section === section.id);
44
+ if (!sectionTools.length) return null;
45
+
46
+ return html`
47
+ <div class="bg-surface border border-border rounded-xl overflow-hidden">
48
+ <h3 class="card-label text-xs px-4 pt-3 pb-2">${section.label}</h3>
49
+ <div class="divide-y divide-border">
50
+ ${sectionTools.map(
51
+ (tool) =>
52
+ html`<${ToolRow}
53
+ key=${tool.id}
54
+ tool=${tool}
55
+ onToggle=${onToggle}
56
+ />`,
57
+ )}
58
+ </div>
59
+ </div>
60
+ `;
61
+ };
62
+
63
+ export const AgentToolsPanel = ({ agent = {}, tools = {} }) => {
64
+ const { profile, toolStates, setProfile, toggleTool } = tools;
65
+
66
+ const enabledTotal = (toolStates || []).filter((t) => t.enabled).length;
67
+ const totalTools = (toolStates || []).length;
68
+
69
+ return html`
70
+ <div class="space-y-4">
71
+ <div class="bg-surface border border-border rounded-xl p-4 space-y-4">
72
+ <div>
73
+ <div class="flex items-center justify-between mb-3">
74
+ <h3 class="card-label text-xs">Preset</h3>
75
+ <span class="text-xs text-gray-500"
76
+ >${enabledTotal}/${totalTools} tools enabled</span
77
+ >
78
+ </div>
79
+ <${SegmentedControl}
80
+ options=${kProfileOptions}
81
+ value=${profile}
82
+ onChange=${setProfile}
83
+ fullWidth
84
+ className="ac-segmented-control-dark"
85
+ />
86
+ </div>
87
+ </div>
88
+
89
+ <div style="columns: 2; column-gap: 0.75rem;">
90
+ ${kSections.map(
91
+ (section) => html`
92
+ <div style="break-inside: avoid; margin-bottom: 0.75rem;">
93
+ <${ToolSection}
94
+ key=${section.id}
95
+ section=${section}
96
+ toolStates=${toolStates || []}
97
+ onToggle=${toggleTool}
98
+ />
99
+ </div>
100
+ `,
101
+ )}
102
+ </div>
103
+ </div>
104
+ `;
105
+ };
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Static tool catalog mirroring OpenClaw's tool-catalog.ts.
3
+ * Grouped and labeled for the AlphaClaw Setup UI.
4
+ */
5
+
6
+ export const kToolProfiles = ["minimal", "messaging", "coding", "full"];
7
+
8
+ export const kProfileLabels = {
9
+ minimal: "Minimal",
10
+ messaging: "Messaging",
11
+ coding: "Coding",
12
+ full: "Full",
13
+ };
14
+
15
+ const kTools = [
16
+ {
17
+ id: "read",
18
+ label: "Read files",
19
+ profiles: ["coding"],
20
+ section: "filesystem",
21
+ },
22
+ {
23
+ id: "edit",
24
+ label: "Edit files",
25
+ profiles: ["coding"],
26
+ section: "filesystem",
27
+ },
28
+ {
29
+ id: "write",
30
+ label: "Write files",
31
+ profiles: ["coding"],
32
+ section: "filesystem",
33
+ },
34
+ {
35
+ id: "apply_patch",
36
+ label: "Apply patches",
37
+ help: "Make targeted patch edits, mainly for OpenAI-compatible patch workflows.",
38
+ profiles: ["coding"],
39
+ section: "filesystem",
40
+ },
41
+ {
42
+ id: "exec",
43
+ label: "Run commands",
44
+ help: "Execute shell commands inside the agent environment.",
45
+ profiles: ["coding"],
46
+ section: "execution",
47
+ },
48
+ {
49
+ id: "process",
50
+ label: "Manage processes",
51
+ help: "Inspect and control long-running background processes.",
52
+ profiles: ["coding"],
53
+ section: "execution",
54
+ },
55
+ {
56
+ id: "message",
57
+ label: "Send messages",
58
+ help: "Send outbound messages through configured messaging channels.",
59
+ profiles: ["messaging"],
60
+ section: "communication",
61
+ },
62
+ {
63
+ id: "tts",
64
+ label: "Text-to-speech",
65
+ help: "Convert text responses into generated speech audio.",
66
+ profiles: [],
67
+ section: "communication",
68
+ },
69
+ {
70
+ id: "browser",
71
+ label: "Control browser",
72
+ help: "Drive a browser for page navigation and interactive web tasks.",
73
+ profiles: [],
74
+ section: "web",
75
+ },
76
+ {
77
+ id: "web_search",
78
+ label: "Search the web",
79
+ help: "Run web searches to discover external information.",
80
+ profiles: [],
81
+ section: "web",
82
+ },
83
+ {
84
+ id: "web_fetch",
85
+ label: "Fetch URLs",
86
+ help: "Fetch and read webpage content from a specific URL.",
87
+ profiles: [],
88
+ section: "web",
89
+ },
90
+ {
91
+ id: "memory_search",
92
+ label: "Semantic search",
93
+ help: "Search memory semantically to find related notes and prior context.",
94
+ profiles: ["coding"],
95
+ section: "memory",
96
+ },
97
+ {
98
+ id: "memory_get",
99
+ label: "Read memories",
100
+ help: "Read stored memory files and saved context entries.",
101
+ profiles: ["coding"],
102
+ section: "memory",
103
+ },
104
+ {
105
+ id: "agents_list",
106
+ label: "List agents",
107
+ help: "List known agent IDs that can be targeted in multi-agent flows.",
108
+ profiles: [],
109
+ section: "multiagent",
110
+ },
111
+ {
112
+ id: "sessions_spawn",
113
+ label: "Spawn sessions",
114
+ help: "Start a new background session/run; this is the base primitive used by sub-agent workflows.",
115
+ profiles: ["coding"],
116
+ section: "multiagent",
117
+ },
118
+ {
119
+ id: "sessions_send",
120
+ label: "Send to session",
121
+ help: "Send messages or tasks into an existing running session.",
122
+ profiles: ["coding", "messaging"],
123
+ section: "multiagent",
124
+ },
125
+ {
126
+ id: "sessions_list",
127
+ label: "List sessions",
128
+ help: "List active or recent sessions available to the agent.",
129
+ profiles: ["coding", "messaging"],
130
+ section: "multiagent",
131
+ },
132
+ {
133
+ id: "sessions_history",
134
+ label: "Session history",
135
+ help: "Read the transcript and prior exchanges from a session.",
136
+ profiles: ["coding", "messaging"],
137
+ section: "multiagent",
138
+ },
139
+ {
140
+ id: "session_status",
141
+ label: "Session status",
142
+ help: "Check whether a session is running and inspect runtime health/state.",
143
+ profiles: ["minimal", "coding", "messaging"],
144
+ section: "multiagent",
145
+ },
146
+ {
147
+ id: "subagents",
148
+ label: "Sub-agents",
149
+ help: "Launch specialized delegated agents (higher-level orchestration built on session spawning).",
150
+ profiles: ["coding"],
151
+ section: "multiagent",
152
+ },
153
+ {
154
+ id: "cron",
155
+ label: "Scheduled jobs",
156
+ help: "Create and manage scheduled automation jobs.",
157
+ profiles: ["coding"],
158
+ section: "scheduling",
159
+ },
160
+ {
161
+ id: "gateway",
162
+ label: "Gateway control",
163
+ help: "Inspect and control the running Gateway service (status, health, and control actions like restart).",
164
+ profiles: [],
165
+ section: "scheduling",
166
+ },
167
+ {
168
+ id: "image",
169
+ label: "Generate images",
170
+ help: "Generate or analyze images with image-capable model tools.",
171
+ profiles: ["coding"],
172
+ section: "creative",
173
+ },
174
+ {
175
+ id: "canvas",
176
+ label: "Visual canvas",
177
+ help: "Control the Canvas panel (present, navigate, eval, snapshot). Primarily a macOS app capability when a canvas-capable node is connected.",
178
+ profiles: [],
179
+ section: "creative",
180
+ },
181
+ {
182
+ id: "nodes",
183
+ label: "Node workflows",
184
+ help: "Use paired device/node capabilities (for example canvas, camera, notifications, and system actions).",
185
+ profiles: [],
186
+ section: "creative",
187
+ },
188
+ ];
189
+
190
+ export const kSections = [
191
+ {
192
+ id: "filesystem",
193
+ label: "Filesystem",
194
+ description: "Read, edit, and write files",
195
+ },
196
+ {
197
+ id: "execution",
198
+ label: "Execution",
199
+ description: "Run shell commands and scripts",
200
+ },
201
+ {
202
+ id: "communication",
203
+ label: "Communication",
204
+ description: "Send messages across Telegram, Slack, Discord",
205
+ },
206
+ {
207
+ id: "web",
208
+ label: "Web & Browser",
209
+ description: "Browse pages, search the web, fetch URLs",
210
+ },
211
+ {
212
+ id: "memory",
213
+ label: "Memory",
214
+ description:
215
+ "Semantic search and retrieval across the agent's stored knowledge",
216
+ },
217
+ {
218
+ id: "multiagent",
219
+ label: "Multi-Agent",
220
+ description:
221
+ "List agents, spawn sessions, send messages between agents. Orchestrate sub-agents.",
222
+ },
223
+ {
224
+ id: "scheduling",
225
+ label: "Scheduling",
226
+ description: "Create and manage scheduled jobs",
227
+ },
228
+ {
229
+ id: "creative",
230
+ label: "Creative",
231
+ description: "Generate images, visual canvas, node-based workflows",
232
+ },
233
+ ];
234
+
235
+ export const getToolsForSection = (sectionId) =>
236
+ kTools.filter((t) => t.section === sectionId);
237
+
238
+ export const getAllToolIds = () => kTools.map((t) => t.id);
239
+
240
+ export const getProfileToolIds = (profileId) => {
241
+ if (profileId === "full") return kTools.map((t) => t.id);
242
+ return kTools.filter((t) => t.profiles.includes(profileId)).map((t) => t.id);
243
+ };
244
+
245
+ /**
246
+ * Given a profile + alsoAllow + deny, resolve whether each tool is enabled.
247
+ */
248
+ export const resolveToolStates = ({
249
+ profile = "full",
250
+ alsoAllow = [],
251
+ deny = [],
252
+ }) => {
253
+ const profileTools = new Set(getProfileToolIds(profile));
254
+ const alsoAllowSet = new Set(alsoAllow);
255
+ const denySet = new Set(deny);
256
+
257
+ return kTools.map((tool) => {
258
+ const inProfile = profileTools.has(tool.id);
259
+ const isDenied = denySet.has(tool.id);
260
+ const isAlsoAllowed = alsoAllowSet.has(tool.id);
261
+ const enabled = isDenied ? false : inProfile || isAlsoAllowed;
262
+
263
+ return { ...tool, enabled, inProfile, isDenied, isAlsoAllowed };
264
+ });
265
+ };
266
+
267
+ /**
268
+ * Derive the minimal tools config from the resolved tool states
269
+ * relative to the selected profile.
270
+ */
271
+ export const deriveToolsConfig = ({ profile, toolStates }) => {
272
+ const profileTools = new Set(getProfileToolIds(profile));
273
+ const alsoAllow = [];
274
+ const deny = [];
275
+
276
+ for (const tool of toolStates) {
277
+ const inProfile = profileTools.has(tool.id);
278
+ if (tool.enabled && !inProfile) {
279
+ alsoAllow.push(tool.id);
280
+ } else if (!tool.enabled && inProfile) {
281
+ deny.push(tool.id);
282
+ }
283
+ }
284
+
285
+ const config = { profile };
286
+ if (alsoAllow.length) config.alsoAllow = alsoAllow;
287
+ if (deny.length) config.deny = deny;
288
+ return config;
289
+ };