@desplega.ai/agent-swarm 1.2.1 → 1.9.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 (119) hide show
  1. package/.claude/settings.local.json +20 -1
  2. package/.env.docker.example +22 -1
  3. package/.env.example +17 -0
  4. package/.github/workflows/docker-publish.yml +92 -0
  5. package/CONTRIBUTING.md +270 -0
  6. package/DEPLOYMENT.md +391 -0
  7. package/Dockerfile.worker +29 -1
  8. package/FAQ.md +19 -0
  9. package/LICENSE +21 -0
  10. package/MCP.md +249 -0
  11. package/README.md +103 -207
  12. package/assets/agent-swarm-logo-orange.png +0 -0
  13. package/assets/agent-swarm-logo.png +0 -0
  14. package/docker-compose.example.yml +137 -0
  15. package/docker-entrypoint.sh +223 -7
  16. package/package.json +8 -3
  17. package/{cc-plugin → plugin}/.claude-plugin/plugin.json +1 -1
  18. package/plugin/README.md +1 -0
  19. package/plugin/agents/.gitkeep +0 -0
  20. package/plugin/agents/codebase-analyzer.md +143 -0
  21. package/plugin/agents/codebase-locator.md +122 -0
  22. package/plugin/agents/codebase-pattern-finder.md +227 -0
  23. package/plugin/agents/web-search-researcher.md +109 -0
  24. package/plugin/commands/create-plan.md +415 -0
  25. package/plugin/commands/implement-plan.md +89 -0
  26. package/plugin/commands/research.md +200 -0
  27. package/plugin/commands/start-leader.md +101 -0
  28. package/plugin/commands/start-worker.md +56 -0
  29. package/plugin/commands/swarm-chat.md +78 -0
  30. package/plugin/commands/todos.md +66 -0
  31. package/plugin/commands/work-on-task.md +44 -0
  32. package/plugin/skills/.gitkeep +0 -0
  33. package/scripts/generate-mcp-docs.ts +415 -0
  34. package/slack-manifest.json +69 -0
  35. package/src/be/db.ts +1431 -25
  36. package/src/cli.tsx +135 -11
  37. package/src/commands/lead.ts +13 -0
  38. package/src/commands/runner.ts +255 -0
  39. package/src/commands/worker.ts +8 -220
  40. package/src/hooks/hook.ts +102 -14
  41. package/src/http.ts +361 -5
  42. package/src/prompts/base-prompt.ts +131 -0
  43. package/src/server.ts +56 -0
  44. package/src/slack/app.ts +73 -0
  45. package/src/slack/commands.ts +88 -0
  46. package/src/slack/handlers.ts +281 -0
  47. package/src/slack/index.ts +3 -0
  48. package/src/slack/responses.ts +175 -0
  49. package/src/slack/router.ts +170 -0
  50. package/src/slack/types.ts +20 -0
  51. package/src/slack/watcher.ts +119 -0
  52. package/src/tools/create-channel.ts +80 -0
  53. package/src/tools/get-tasks.ts +54 -21
  54. package/src/tools/join-swarm.ts +28 -4
  55. package/src/tools/list-channels.ts +37 -0
  56. package/src/tools/list-services.ts +110 -0
  57. package/src/tools/poll-task.ts +46 -3
  58. package/src/tools/post-message.ts +87 -0
  59. package/src/tools/read-messages.ts +192 -0
  60. package/src/tools/register-service.ts +118 -0
  61. package/src/tools/send-task.ts +80 -7
  62. package/src/tools/store-progress.ts +9 -3
  63. package/src/tools/task-action.ts +211 -0
  64. package/src/tools/unregister-service.ts +110 -0
  65. package/src/tools/update-profile.ts +105 -0
  66. package/src/tools/update-service-status.ts +118 -0
  67. package/src/types.ts +110 -3
  68. package/src/utils/pretty-print.ts +224 -0
  69. package/thoughts/shared/plans/.gitkeep +0 -0
  70. package/thoughts/shared/plans/2025-12-18-inverse-teleport.md +1142 -0
  71. package/thoughts/shared/plans/2025-12-18-slack-integration.md +1195 -0
  72. package/thoughts/shared/plans/2025-12-19-agent-log-streaming.md +732 -0
  73. package/thoughts/shared/plans/2025-12-19-role-based-swarm-plugin.md +361 -0
  74. package/thoughts/shared/plans/2025-12-20-mobile-responsive-ui.md +501 -0
  75. package/thoughts/shared/plans/2025-12-20-startup-team-swarm.md +560 -0
  76. package/thoughts/shared/research/.gitkeep +0 -0
  77. package/thoughts/shared/research/2025-12-18-slack-integration.md +442 -0
  78. package/thoughts/shared/research/2025-12-19-agent-log-streaming.md +339 -0
  79. package/thoughts/shared/research/2025-12-19-agent-secrets-cli-research.md +390 -0
  80. package/thoughts/shared/research/2025-12-21-gemini-cli-integration.md +376 -0
  81. package/thoughts/shared/research/2025-12-22-setup-experience-improvements.md +264 -0
  82. package/tsconfig.json +3 -1
  83. package/ui/bun.lock +692 -0
  84. package/ui/index.html +22 -0
  85. package/ui/package.json +32 -0
  86. package/ui/pnpm-lock.yaml +3034 -0
  87. package/ui/postcss.config.js +6 -0
  88. package/ui/public/logo.png +0 -0
  89. package/ui/src/App.tsx +43 -0
  90. package/ui/src/components/ActivityFeed.tsx +415 -0
  91. package/ui/src/components/AgentDetailPanel.tsx +534 -0
  92. package/ui/src/components/AgentsPanel.tsx +549 -0
  93. package/ui/src/components/ChatPanel.tsx +1820 -0
  94. package/ui/src/components/ConfigModal.tsx +232 -0
  95. package/ui/src/components/Dashboard.tsx +534 -0
  96. package/ui/src/components/Header.tsx +168 -0
  97. package/ui/src/components/ServicesPanel.tsx +612 -0
  98. package/ui/src/components/StatsBar.tsx +288 -0
  99. package/ui/src/components/StatusBadge.tsx +124 -0
  100. package/ui/src/components/TaskDetailPanel.tsx +807 -0
  101. package/ui/src/components/TasksPanel.tsx +575 -0
  102. package/ui/src/hooks/queries.ts +170 -0
  103. package/ui/src/index.css +235 -0
  104. package/ui/src/lib/api.ts +161 -0
  105. package/ui/src/lib/config.ts +35 -0
  106. package/ui/src/lib/theme.ts +214 -0
  107. package/ui/src/lib/utils.ts +48 -0
  108. package/ui/src/main.tsx +32 -0
  109. package/ui/src/types/api.ts +164 -0
  110. package/ui/src/vite-env.d.ts +1 -0
  111. package/ui/tailwind.config.js +35 -0
  112. package/ui/tsconfig.json +31 -0
  113. package/ui/vite.config.ts +22 -0
  114. package/cc-plugin/README.md +0 -49
  115. package/cc-plugin/commands/setup-leader.md +0 -73
  116. package/cc-plugin/commands/start-worker.md +0 -64
  117. package/docker-compose.worker.yml +0 -35
  118. package/example-req-meta.json +0 -24
  119. /package/{cc-plugin → plugin}/hooks/hooks.json +0 -0
@@ -0,0 +1,170 @@
1
+ import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from "@tanstack/react-query";
2
+ import { api } from "../lib/api";
3
+ import type { AgentWithTasks } from "../types/api";
4
+
5
+ export function useAgents() {
6
+ return useQuery({
7
+ queryKey: ["agents"],
8
+ queryFn: () => api.fetchAgents(true),
9
+ select: (data) => data.agents as AgentWithTasks[],
10
+ });
11
+ }
12
+
13
+ export function useAgent(id: string) {
14
+ return useQuery({
15
+ queryKey: ["agent", id],
16
+ queryFn: () => api.fetchAgent(id),
17
+ enabled: !!id,
18
+ });
19
+ }
20
+
21
+ export interface TaskFilters {
22
+ status?: string;
23
+ agentId?: string;
24
+ search?: string;
25
+ }
26
+
27
+ export function useTasks(filters?: TaskFilters) {
28
+ return useQuery({
29
+ queryKey: ["tasks", filters],
30
+ queryFn: () => api.fetchTasks(filters),
31
+ select: (data) => data.tasks,
32
+ });
33
+ }
34
+
35
+ export function useTask(id: string) {
36
+ return useQuery({
37
+ queryKey: ["task", id],
38
+ queryFn: () => api.fetchTask(id),
39
+ enabled: !!id,
40
+ });
41
+ }
42
+
43
+ export function useLogs(limit = 50, agentId?: string) {
44
+ return useQuery({
45
+ queryKey: ["logs", limit, agentId],
46
+ queryFn: () => api.fetchLogs(limit, agentId),
47
+ select: (data) => data.logs,
48
+ });
49
+ }
50
+
51
+ export function useStats() {
52
+ return useQuery({
53
+ queryKey: ["stats"],
54
+ queryFn: () => api.fetchStats(),
55
+ });
56
+ }
57
+
58
+ export function useHealth() {
59
+ return useQuery({
60
+ queryKey: ["health"],
61
+ queryFn: () => api.checkHealth(),
62
+ refetchInterval: 10000, // Check every 10 seconds
63
+ retry: 2,
64
+ retryDelay: 1000,
65
+ });
66
+ }
67
+
68
+ export function useChannels() {
69
+ return useQuery({
70
+ queryKey: ["channels"],
71
+ queryFn: () => api.fetchChannels(),
72
+ select: (data) => data.channels,
73
+ });
74
+ }
75
+
76
+ export interface MessageFilters {
77
+ limit?: number;
78
+ since?: string;
79
+ before?: string;
80
+ }
81
+
82
+ export function useMessages(channelId: string, filters?: MessageFilters) {
83
+ return useQuery({
84
+ queryKey: ["messages", channelId, filters],
85
+ queryFn: () => api.fetchMessages(channelId, filters),
86
+ select: (data) => data.messages,
87
+ enabled: !!channelId,
88
+ });
89
+ }
90
+
91
+ const DEFAULT_MESSAGE_LIMIT = 100;
92
+
93
+ export function useInfiniteMessages(channelId: string, pageSize = DEFAULT_MESSAGE_LIMIT) {
94
+ return useInfiniteQuery({
95
+ queryKey: ["infiniteMessages", channelId],
96
+ queryFn: async ({ pageParam }) => {
97
+ const result = await api.fetchMessages(channelId, {
98
+ limit: pageSize,
99
+ before: pageParam,
100
+ });
101
+ return result.messages;
102
+ },
103
+ initialPageParam: undefined as string | undefined,
104
+ getNextPageParam: (lastPage) => {
105
+ // If we got fewer messages than requested, there are no more
106
+ if (lastPage.length < pageSize) return undefined;
107
+ // Get the oldest message's createdAt for the next page
108
+ const oldest = lastPage[0]; // Messages are in chronological order, first is oldest
109
+ return oldest?.createdAt;
110
+ },
111
+ enabled: !!channelId,
112
+ select: (data) => {
113
+ // Flatten all pages and dedupe by id, keeping chronological order
114
+ const allMessages = data.pages.flat();
115
+ const seen = new Set<string>();
116
+ const deduped = allMessages.filter((msg) => {
117
+ if (seen.has(msg.id)) return false;
118
+ seen.add(msg.id);
119
+ return true;
120
+ });
121
+ // Sort chronologically (oldest first)
122
+ return deduped.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
123
+ },
124
+ });
125
+ }
126
+
127
+ export function useThreadMessages(channelId: string, messageId: string) {
128
+ return useQuery({
129
+ queryKey: ["thread", channelId, messageId],
130
+ queryFn: () => api.fetchThreadMessages(channelId, messageId),
131
+ select: (data) => data.messages,
132
+ enabled: !!channelId && !!messageId,
133
+ });
134
+ }
135
+
136
+ export function usePostMessage(channelId: string) {
137
+ const queryClient = useQueryClient();
138
+
139
+ return useMutation({
140
+ mutationFn: (params: { content: string; agentId?: string; replyToId?: string; mentions?: string[] }) =>
141
+ api.postMessage(channelId, params.content, {
142
+ agentId: params.agentId,
143
+ replyToId: params.replyToId,
144
+ mentions: params.mentions,
145
+ }),
146
+ onSuccess: (_data, variables) => {
147
+ // Invalidate channel messages (both regular and infinite)
148
+ queryClient.invalidateQueries({ queryKey: ["messages", channelId] });
149
+ queryClient.invalidateQueries({ queryKey: ["infiniteMessages", channelId] });
150
+ // Also invalidate thread if this was a reply
151
+ if (variables.replyToId) {
152
+ queryClient.invalidateQueries({ queryKey: ["thread", channelId, variables.replyToId] });
153
+ }
154
+ },
155
+ });
156
+ }
157
+
158
+ export interface ServiceFilters {
159
+ status?: string;
160
+ agentId?: string;
161
+ name?: string;
162
+ }
163
+
164
+ export function useServices(filters?: ServiceFilters) {
165
+ return useQuery({
166
+ queryKey: ["services", filters],
167
+ queryFn: () => api.fetchServices(filters),
168
+ select: (data) => data.services,
169
+ });
170
+ }
@@ -0,0 +1,235 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ /* Beehive color palette - dark mode defaults */
7
+ --hive-amber: #F5A623;
8
+ --hive-honey: #FFB84D;
9
+ --hive-gold: #D4A574;
10
+ --hive-deep: #C67C00;
11
+ --hive-rust: #A85454;
12
+ --hive-blue: #3B82F6;
13
+ --hive-cream: #FFF8E7;
14
+ --hive-body: #0D0906;
15
+ --hive-surface: #1A130E;
16
+ --hive-earth: #251C15;
17
+ --hive-border: #3A2D1F;
18
+ --hive-text-primary: #FFF8E7;
19
+ --hive-text-secondary: #C9B896;
20
+ --hive-text-tertiary: #8B7355;
21
+
22
+ font-family: "Space Grotesk", sans-serif;
23
+ font-synthesis: none;
24
+ text-rendering: optimizeLegibility;
25
+ -webkit-font-smoothing: antialiased;
26
+ -moz-osx-font-smoothing: grayscale;
27
+ }
28
+
29
+ /* Light mode color overrides */
30
+ [data-joy-color-scheme="light"] {
31
+ --hive-amber: #D48806;
32
+ --hive-honey: #B87300;
33
+ --hive-gold: #8B6914;
34
+ --hive-deep: #9A5F00;
35
+ --hive-rust: #B54242;
36
+ --hive-cream: #1A130E;
37
+ --hive-body: #FDF8F3;
38
+ --hive-surface: #FFFFFF;
39
+ --hive-earth: #F5EDE4;
40
+ --hive-border: #E5D9CA;
41
+ --hive-text-primary: #1A130E;
42
+ --hive-text-secondary: #5C4A3D;
43
+ --hive-text-tertiary: #8B7355;
44
+ }
45
+
46
+ body {
47
+ margin: 0;
48
+ background: var(--hive-body);
49
+ color: var(--hive-text-primary);
50
+ min-height: 100vh;
51
+ }
52
+
53
+ /* Honeycomb pattern background */
54
+ .honeycomb-bg {
55
+ position: relative;
56
+ }
57
+
58
+ .honeycomb-bg::before {
59
+ content: "";
60
+ position: absolute;
61
+ inset: 0;
62
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='49' viewBox='0 0 28 49'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%23F5A623' fill-opacity='0.08'%3E%3Cpath d='M13.99 9.25l13 7.5v15l-13 7.5L1 31.75v-15l12.99-7.5zM3 17.9v12.7l10.99 6.34 11-6.35V17.9l-11-6.34L3 17.9zM0 15l12.98-7.5V0h-2v6.35L0 12.69v2.3zm0 18.5L12.98 41v8h-2v-6.85L0 35.81v-2.3zM15 0v7.5L27.99 15H28v-2.31h-.01L17 6.35V0h-2zm0 49v-8l12.99-7.5H28v2.31h-.01L17 42.15V49h-2z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
63
+ pointer-events: none;
64
+ z-index: 0;
65
+ }
66
+
67
+ /* Light mode honeycomb pattern - darker color */
68
+ [data-joy-color-scheme="light"] .honeycomb-bg::before {
69
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='49' viewBox='0 0 28 49'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%23D48806' fill-opacity='0.1'%3E%3Cpath d='M13.99 9.25l13 7.5v15l-13 7.5L1 31.75v-15l12.99-7.5zM3 17.9v12.7l10.99 6.34 11-6.35V17.9l-11-6.34L3 17.9zM0 15l12.98-7.5V0h-2v6.35L0 12.69v2.3zm0 18.5L12.98 41v8h-2v-6.85L0 35.81v-2.3zM15 0v7.5L27.99 15H28v-2.31h-.01L17 6.35V0h-2zm0 49v-8l12.99-7.5H28v2.31h-.01L17 42.15V49h-2z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
70
+ }
71
+
72
+ .honeycomb-bg > * {
73
+ position: relative;
74
+ z-index: 1;
75
+ }
76
+
77
+ /* Warm amber scrollbar */
78
+ ::-webkit-scrollbar {
79
+ width: 8px;
80
+ height: 8px;
81
+ }
82
+
83
+ ::-webkit-scrollbar-track {
84
+ background: var(--hive-body);
85
+ }
86
+
87
+ ::-webkit-scrollbar-thumb {
88
+ background: var(--hive-border);
89
+ border-radius: 4px;
90
+ }
91
+
92
+ ::-webkit-scrollbar-thumb:hover {
93
+ background: var(--hive-text-tertiary);
94
+ }
95
+
96
+ /* Amber glow effects */
97
+ .glow-amber {
98
+ text-shadow: 0 0 10px rgba(245, 166, 35, 0.5), 0 0 20px rgba(245, 166, 35, 0.3);
99
+ }
100
+
101
+ .glow-gold {
102
+ text-shadow: 0 0 10px rgba(212, 165, 116, 0.5), 0 0 20px rgba(212, 165, 116, 0.3);
103
+ }
104
+
105
+ .glow-rust {
106
+ text-shadow: 0 0 10px rgba(168, 84, 84, 0.5), 0 0 20px rgba(168, 84, 84, 0.3);
107
+ }
108
+
109
+ .glow-blue {
110
+ text-shadow: 0 0 10px rgba(59, 130, 246, 0.5), 0 0 20px rgba(59, 130, 246, 0.3);
111
+ }
112
+
113
+ /* Box glow effects */
114
+ .box-glow-amber {
115
+ box-shadow: 0 0 15px rgba(245, 166, 35, 0.3), 0 0 30px rgba(245, 166, 35, 0.15);
116
+ }
117
+
118
+ .box-glow-gold {
119
+ box-shadow: 0 0 15px rgba(212, 165, 116, 0.3), 0 0 30px rgba(212, 165, 116, 0.15);
120
+ }
121
+
122
+ /* Breathing animation for active elements */
123
+ @keyframes breathe {
124
+ 0%, 100% {
125
+ opacity: 0.85;
126
+ transform: scale(1);
127
+ }
128
+ 50% {
129
+ opacity: 1;
130
+ transform: scale(1.02);
131
+ }
132
+ }
133
+
134
+ .animate-breathe {
135
+ animation: breathe 3s ease-in-out infinite;
136
+ }
137
+
138
+ /* Pulse animation */
139
+ @keyframes pulse-amber {
140
+ 0%, 100% {
141
+ box-shadow: 0 0 5px rgba(245, 166, 35, 0.4);
142
+ }
143
+ 50% {
144
+ box-shadow: 0 0 15px rgba(245, 166, 35, 0.6), 0 0 25px rgba(245, 166, 35, 0.3);
145
+ }
146
+ }
147
+
148
+ .animate-pulse-amber {
149
+ animation: pulse-amber 2s ease-in-out infinite;
150
+ }
151
+
152
+ /* Heartbeat animation for connection status */
153
+ @keyframes heartbeat {
154
+ 0%, 100% {
155
+ transform: scale(1);
156
+ }
157
+ 14% {
158
+ transform: scale(1.1);
159
+ }
160
+ 28% {
161
+ transform: scale(1);
162
+ }
163
+ 42% {
164
+ transform: scale(1.1);
165
+ }
166
+ 70% {
167
+ transform: scale(1);
168
+ }
169
+ }
170
+
171
+ .animate-heartbeat {
172
+ animation: heartbeat 1.5s ease-in-out infinite;
173
+ }
174
+
175
+ /* Hexagonal clip path utility */
176
+ .clip-hex {
177
+ clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
178
+ }
179
+
180
+ /* Hexagonal border (using drop-shadow trick) */
181
+ .hex-border {
182
+ position: relative;
183
+ }
184
+
185
+ .hex-border::before {
186
+ content: "";
187
+ position: absolute;
188
+ inset: -2px;
189
+ clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
190
+ background: linear-gradient(135deg, var(--hive-amber) 0%, var(--hive-deep) 100%);
191
+ z-index: -1;
192
+ }
193
+
194
+ /* Gradient text for headers */
195
+ .text-gradient-amber {
196
+ background: linear-gradient(135deg, var(--hive-amber) 0%, var(--hive-honey) 50%, var(--hive-deep) 100%);
197
+ -webkit-background-clip: text;
198
+ background-clip: text;
199
+ -webkit-text-fill-color: transparent;
200
+ }
201
+
202
+ /* Card hover effect */
203
+ .card-hover {
204
+ transition: all 0.3s ease;
205
+ }
206
+
207
+ .card-hover:hover {
208
+ border-color: rgba(245, 166, 35, 0.3);
209
+ box-shadow: 0 0 20px rgba(245, 166, 35, 0.1);
210
+ }
211
+
212
+ /* Table row hover */
213
+ .row-hover {
214
+ transition: background-color 0.2s ease;
215
+ }
216
+
217
+ .row-hover:hover {
218
+ background-color: rgba(245, 166, 35, 0.05);
219
+ }
220
+
221
+ /* Focus ring */
222
+ .focus-amber:focus-visible {
223
+ outline: 2px solid var(--hive-amber);
224
+ outline-offset: 2px;
225
+ }
226
+
227
+ /* Subtle grain overlay for texture */
228
+ .grain-overlay::after {
229
+ content: "";
230
+ position: absolute;
231
+ inset: 0;
232
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.02'/%3E%3C/svg%3E");
233
+ pointer-events: none;
234
+ z-index: 0;
235
+ }
@@ -0,0 +1,161 @@
1
+ import { getConfig } from "./config";
2
+ import type {
3
+ AgentsResponse,
4
+ TasksResponse,
5
+ LogsResponse,
6
+ ChannelsResponse,
7
+ MessagesResponse,
8
+ ServicesResponse,
9
+ ChannelMessage,
10
+ Stats,
11
+ AgentWithTasks,
12
+ TaskWithLogs,
13
+ } from "../types/api";
14
+
15
+ class ApiClient {
16
+ private getHeaders(): HeadersInit {
17
+ const config = getConfig();
18
+ const headers: HeadersInit = {
19
+ "Content-Type": "application/json",
20
+ };
21
+ if (config.apiKey) {
22
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
23
+ }
24
+ return headers;
25
+ }
26
+
27
+ private getBaseUrl(): string {
28
+ const config = getConfig();
29
+ // In development, use relative URL (proxied by Vite)
30
+ // In production, use configured API URL
31
+ if (import.meta.env.DEV && config.apiUrl === "http://localhost:3013") {
32
+ return "";
33
+ }
34
+ return config.apiUrl;
35
+ }
36
+
37
+ async fetchAgents(includeTasks = true): Promise<AgentsResponse> {
38
+ const url = `${this.getBaseUrl()}/api/agents${includeTasks ? "?include=tasks" : ""}`;
39
+ const res = await fetch(url, { headers: this.getHeaders() });
40
+ if (!res.ok) throw new Error(`Failed to fetch agents: ${res.status}`);
41
+ return res.json();
42
+ }
43
+
44
+ async fetchAgent(id: string, includeTasks = true): Promise<AgentWithTasks> {
45
+ const url = `${this.getBaseUrl()}/api/agents/${id}${includeTasks ? "?include=tasks" : ""}`;
46
+ const res = await fetch(url, { headers: this.getHeaders() });
47
+ if (!res.ok) throw new Error(`Failed to fetch agent: ${res.status}`);
48
+ return res.json();
49
+ }
50
+
51
+ async fetchTasks(filters?: { status?: string; agentId?: string; search?: string }): Promise<TasksResponse> {
52
+ const params = new URLSearchParams();
53
+ if (filters?.status) params.set("status", filters.status);
54
+ if (filters?.agentId) params.set("agentId", filters.agentId);
55
+ if (filters?.search) params.set("search", filters.search);
56
+ const queryString = params.toString();
57
+ const url = `${this.getBaseUrl()}/api/tasks${queryString ? `?${queryString}` : ""}`;
58
+ const res = await fetch(url, { headers: this.getHeaders() });
59
+ if (!res.ok) throw new Error(`Failed to fetch tasks: ${res.status}`);
60
+ return res.json();
61
+ }
62
+
63
+ async fetchTask(id: string): Promise<TaskWithLogs> {
64
+ const url = `${this.getBaseUrl()}/api/tasks/${id}`;
65
+ const res = await fetch(url, { headers: this.getHeaders() });
66
+ if (!res.ok) throw new Error(`Failed to fetch task: ${res.status}`);
67
+ return res.json();
68
+ }
69
+
70
+ async fetchLogs(limit = 100, agentId?: string): Promise<LogsResponse> {
71
+ const params = new URLSearchParams();
72
+ params.set("limit", String(limit));
73
+ if (agentId) params.set("agentId", agentId);
74
+ const url = `${this.getBaseUrl()}/api/logs?${params.toString()}`;
75
+ const res = await fetch(url, { headers: this.getHeaders() });
76
+ if (!res.ok) throw new Error(`Failed to fetch logs: ${res.status}`);
77
+ return res.json();
78
+ }
79
+
80
+ async fetchStats(): Promise<Stats> {
81
+ const url = `${this.getBaseUrl()}/api/stats`;
82
+ const res = await fetch(url, { headers: this.getHeaders() });
83
+ if (!res.ok) throw new Error(`Failed to fetch stats: ${res.status}`);
84
+ return res.json();
85
+ }
86
+
87
+ async checkHealth(): Promise<{ status: string; version: string }> {
88
+ // Health endpoint is not under /api, so we need to handle it specially
89
+ const config = getConfig();
90
+ const baseUrl = import.meta.env.DEV && config.apiUrl === "http://localhost:3013"
91
+ ? "http://localhost:3013"
92
+ : config.apiUrl;
93
+ const url = `${baseUrl}/health`;
94
+ const res = await fetch(url, { headers: this.getHeaders() });
95
+ if (!res.ok) throw new Error(`Health check failed: ${res.status}`);
96
+ return res.json();
97
+ }
98
+
99
+ async fetchChannels(): Promise<ChannelsResponse> {
100
+ const url = `${this.getBaseUrl()}/api/channels`;
101
+ const res = await fetch(url, { headers: this.getHeaders() });
102
+ if (!res.ok) throw new Error(`Failed to fetch channels: ${res.status}`);
103
+ return res.json();
104
+ }
105
+
106
+ async fetchMessages(
107
+ channelId: string,
108
+ options?: { limit?: number; since?: string; before?: string }
109
+ ): Promise<MessagesResponse> {
110
+ const params = new URLSearchParams();
111
+ if (options?.limit) params.set("limit", String(options.limit));
112
+ if (options?.since) params.set("since", options.since);
113
+ if (options?.before) params.set("before", options.before);
114
+ const queryString = params.toString();
115
+ const url = `${this.getBaseUrl()}/api/channels/${channelId}/messages${queryString ? `?${queryString}` : ""}`;
116
+ const res = await fetch(url, { headers: this.getHeaders() });
117
+ if (!res.ok) throw new Error(`Failed to fetch messages: ${res.status}`);
118
+ return res.json();
119
+ }
120
+
121
+ async fetchThreadMessages(channelId: string, messageId: string): Promise<MessagesResponse> {
122
+ const url = `${this.getBaseUrl()}/api/channels/${channelId}/messages/${messageId}/thread`;
123
+ const res = await fetch(url, { headers: this.getHeaders() });
124
+ if (!res.ok) throw new Error(`Failed to fetch thread: ${res.status}`);
125
+ return res.json();
126
+ }
127
+
128
+ async postMessage(
129
+ channelId: string,
130
+ content: string,
131
+ options?: { agentId?: string; replyToId?: string; mentions?: string[] }
132
+ ): Promise<ChannelMessage> {
133
+ const url = `${this.getBaseUrl()}/api/channels/${channelId}/messages`;
134
+ const res = await fetch(url, {
135
+ method: "POST",
136
+ headers: this.getHeaders(),
137
+ body: JSON.stringify({
138
+ content,
139
+ agentId: options?.agentId,
140
+ replyToId: options?.replyToId,
141
+ mentions: options?.mentions,
142
+ }),
143
+ });
144
+ if (!res.ok) throw new Error(`Failed to post message: ${res.status}`);
145
+ return res.json();
146
+ }
147
+
148
+ async fetchServices(filters?: { status?: string; agentId?: string; name?: string }): Promise<ServicesResponse> {
149
+ const params = new URLSearchParams();
150
+ if (filters?.status) params.set("status", filters.status);
151
+ if (filters?.agentId) params.set("agentId", filters.agentId);
152
+ if (filters?.name) params.set("name", filters.name);
153
+ const queryString = params.toString();
154
+ const url = `${this.getBaseUrl()}/api/services${queryString ? `?${queryString}` : ""}`;
155
+ const res = await fetch(url, { headers: this.getHeaders() });
156
+ if (!res.ok) throw new Error(`Failed to fetch services: ${res.status}`);
157
+ return res.json();
158
+ }
159
+ }
160
+
161
+ export const api = new ApiClient();
@@ -0,0 +1,35 @@
1
+ const STORAGE_KEY = "agent-swarm-config";
2
+
3
+ export interface Config {
4
+ apiUrl: string;
5
+ apiKey: string;
6
+ }
7
+
8
+ const DEFAULT_CONFIG: Config = {
9
+ apiUrl: "http://localhost:3013",
10
+ apiKey: "",
11
+ };
12
+
13
+ export function getConfig(): Config {
14
+ try {
15
+ const stored = localStorage.getItem(STORAGE_KEY);
16
+ if (stored) {
17
+ return { ...DEFAULT_CONFIG, ...JSON.parse(stored) };
18
+ }
19
+ } catch (e) {
20
+ console.error("Failed to load config:", e);
21
+ }
22
+ return DEFAULT_CONFIG;
23
+ }
24
+
25
+ export function saveConfig(config: Config): void {
26
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
27
+ }
28
+
29
+ export function resetConfig(): void {
30
+ localStorage.removeItem(STORAGE_KEY);
31
+ }
32
+
33
+ export function getDefaultConfig(): Config {
34
+ return { ...DEFAULT_CONFIG };
35
+ }