@swarmclawai/swarmclaw 0.3.0 → 0.4.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 (118) hide show
  1. package/README.md +20 -11
  2. package/bin/server-cmd.js +14 -7
  3. package/bin/swarmclaw.js +3 -1
  4. package/bin/update-cmd.js +120 -0
  5. package/next.config.ts +2 -0
  6. package/package.json +3 -1
  7. package/src/app/api/agents/[id]/route.ts +3 -0
  8. package/src/app/api/agents/[id]/thread/route.ts +2 -1
  9. package/src/app/api/agents/route.ts +5 -1
  10. package/src/app/api/auth/route.ts +3 -1
  11. package/src/app/api/claude-skills/route.ts +3 -1
  12. package/src/app/api/connectors/[id]/route.ts +4 -0
  13. package/src/app/api/connectors/route.ts +6 -1
  14. package/src/app/api/credentials/route.ts +3 -1
  15. package/src/app/api/daemon/route.ts +6 -1
  16. package/src/app/api/ip/route.ts +3 -1
  17. package/src/app/api/mcp-servers/route.ts +3 -1
  18. package/src/app/api/orchestrator/graph/route.ts +25 -0
  19. package/src/app/api/plugins/marketplace/route.ts +3 -1
  20. package/src/app/api/plugins/route.ts +3 -1
  21. package/src/app/api/providers/[id]/route.ts +3 -0
  22. package/src/app/api/providers/configs/route.ts +3 -1
  23. package/src/app/api/providers/route.ts +5 -1
  24. package/src/app/api/schedules/[id]/route.ts +3 -0
  25. package/src/app/api/schedules/route.ts +6 -1
  26. package/src/app/api/secrets/route.ts +3 -1
  27. package/src/app/api/sessions/[id]/chat/route.ts +5 -2
  28. package/src/app/api/sessions/route.ts +9 -2
  29. package/src/app/api/settings/route.ts +3 -1
  30. package/src/app/api/setup/doctor/route.ts +1 -0
  31. package/src/app/api/setup/openclaw-device/route.ts +3 -1
  32. package/src/app/api/skills/route.ts +3 -1
  33. package/src/app/api/tasks/[id]/approve/route.ts +73 -0
  34. package/src/app/api/tasks/[id]/route.ts +3 -0
  35. package/src/app/api/tasks/route.ts +3 -0
  36. package/src/app/api/usage/route.ts +3 -1
  37. package/src/app/api/version/route.ts +3 -1
  38. package/src/app/api/webhooks/[id]/route.ts +2 -1
  39. package/src/app/api/webhooks/route.ts +3 -1
  40. package/src/app/icon.svg +58 -0
  41. package/src/app/page.tsx +8 -2
  42. package/src/cli/index.js +1 -9
  43. package/src/cli/index.ts +51 -1
  44. package/src/cli/spec.js +0 -8
  45. package/src/components/agents/agent-card.tsx +1 -1
  46. package/src/components/agents/agent-sheet.tsx +63 -80
  47. package/src/components/chat/chat-area.tsx +44 -30
  48. package/src/components/chat/chat-tool-toggles.tsx +12 -53
  49. package/src/components/chat/message-bubble.tsx +110 -42
  50. package/src/components/chat/tool-call-bubble.tsx +41 -3
  51. package/src/components/chat/tool-request-banner.tsx +1 -9
  52. package/src/components/connectors/connector-list.tsx +3 -8
  53. package/src/components/connectors/connector-sheet.tsx +24 -29
  54. package/src/components/input/chat-input.tsx +72 -56
  55. package/src/components/knowledge/knowledge-list.tsx +27 -31
  56. package/src/components/layout/app-layout.tsx +92 -71
  57. package/src/components/layout/daemon-indicator.tsx +3 -5
  58. package/src/components/logs/log-list.tsx +5 -9
  59. package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
  60. package/src/components/memory/memory-detail.tsx +1 -1
  61. package/src/components/plugins/plugin-list.tsx +227 -27
  62. package/src/components/providers/provider-list.tsx +46 -13
  63. package/src/components/providers/provider-sheet.tsx +0 -45
  64. package/src/components/runs/run-list.tsx +6 -15
  65. package/src/components/schedules/schedule-card.tsx +54 -4
  66. package/src/components/schedules/schedule-list.tsx +6 -3
  67. package/src/components/schedules/schedule-sheet.tsx +0 -47
  68. package/src/components/secrets/secrets-list.tsx +20 -2
  69. package/src/components/sessions/new-session-sheet.tsx +8 -9
  70. package/src/components/shared/connector-platform-icon.tsx +22 -20
  71. package/src/components/shared/model-combobox.tsx +148 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +7 -39
  73. package/src/components/shared/settings/section-orchestrator.tsx +8 -9
  74. package/src/components/skills/skill-list.tsx +260 -34
  75. package/src/components/skills/skill-sheet.tsx +0 -45
  76. package/src/components/tasks/task-board.tsx +3 -6
  77. package/src/components/tasks/task-card.tsx +43 -1
  78. package/src/components/tasks/task-list.tsx +3 -5
  79. package/src/components/tasks/task-sheet.tsx +0 -44
  80. package/src/components/usage/usage-list.tsx +12 -4
  81. package/src/hooks/use-ws.ts +66 -0
  82. package/src/instrumentation.ts +2 -0
  83. package/src/lib/chat.ts +14 -2
  84. package/src/lib/providers/anthropic.ts +1 -1
  85. package/src/lib/providers/index.ts +2 -0
  86. package/src/lib/providers/ollama.ts +1 -1
  87. package/src/lib/providers/openai.ts +33 -12
  88. package/src/lib/server/chat-execution.ts +19 -4
  89. package/src/lib/server/connectors/manager.ts +9 -3
  90. package/src/lib/server/context-manager.ts +1 -1
  91. package/src/lib/server/daemon-state.ts +3 -0
  92. package/src/lib/server/data-dir.ts +1 -0
  93. package/src/lib/server/heartbeat-service.ts +67 -3
  94. package/src/lib/server/langgraph-checkpoint.ts +274 -0
  95. package/src/lib/server/main-agent-loop.ts +61 -2
  96. package/src/lib/server/orchestrator-lg.ts +394 -13
  97. package/src/lib/server/orchestrator.ts +25 -5
  98. package/src/lib/server/queue.ts +17 -3
  99. package/src/lib/server/session-run-manager.ts +6 -1
  100. package/src/lib/server/session-tools/delegate.ts +2 -2
  101. package/src/lib/server/session-tools/index.ts +2 -0
  102. package/src/lib/server/session-tools/sandbox.ts +164 -0
  103. package/src/lib/server/storage-mcp.test.ts +25 -2
  104. package/src/lib/server/storage.ts +24 -7
  105. package/src/lib/server/stream-agent-chat.ts +77 -22
  106. package/src/lib/server/task-validation.test.ts +23 -0
  107. package/src/lib/server/task-validation.ts +5 -3
  108. package/src/lib/server/ws-hub.ts +85 -0
  109. package/src/lib/tool-definitions.ts +42 -0
  110. package/src/lib/upload.ts +7 -1
  111. package/src/lib/ws-client.ts +124 -0
  112. package/src/stores/use-chat-store.ts +33 -13
  113. package/src/types/index.ts +8 -1
  114. package/src/app/api/agents/generate/route.ts +0 -42
  115. package/src/app/api/generate/info/route.ts +0 -12
  116. package/src/app/api/generate/route.ts +0 -106
  117. package/src/app/favicon.ico +0 -0
  118. package/src/components/shared/ai-gen-block.tsx +0 -77
package/src/lib/upload.ts CHANGED
@@ -1,10 +1,16 @@
1
1
  import type { UploadResult } from '../types'
2
+ import { getStoredAccessKey } from './api-client'
2
3
 
3
4
  export async function uploadImage(file: File): Promise<UploadResult> {
5
+ const key = getStoredAccessKey()
4
6
  const res = await fetch('/api/upload', {
5
7
  method: 'POST',
6
- headers: { 'X-Filename': file.name },
8
+ headers: {
9
+ 'X-Filename': file.name,
10
+ ...(key ? { 'X-Access-Key': key } : {}),
11
+ },
7
12
  body: file,
8
13
  })
14
+ if (!res.ok) throw new Error(`Upload failed (${res.status})`)
9
15
  return res.json()
10
16
  }
@@ -0,0 +1,124 @@
1
+ type WsCallback = () => void
2
+
3
+ let ws: WebSocket | null = null
4
+ let accessKey = ''
5
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null
6
+ let reconnectDelay = 1000
7
+ const MAX_RECONNECT_DELAY = 30_000
8
+ const listeners = new Map<string, Set<WsCallback>>()
9
+ let connected = false
10
+
11
+ function getWsUrl(key: string): string {
12
+ const host = typeof window !== 'undefined' ? window.location.hostname : 'localhost'
13
+ const port = process.env.NEXT_PUBLIC_WS_PORT || '3457'
14
+ const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss' : 'ws'
15
+ return `${protocol}://${host}:${port}/ws?key=${encodeURIComponent(key)}`
16
+ }
17
+
18
+ function handleMessage(event: MessageEvent) {
19
+ try {
20
+ const msg = JSON.parse(event.data)
21
+ const topic = msg.topic as string
22
+ if (!topic) return
23
+ const cbs = listeners.get(topic)
24
+ if (cbs) {
25
+ for (const cb of cbs) cb()
26
+ }
27
+ } catch {
28
+ // ignore malformed
29
+ }
30
+ }
31
+
32
+ function scheduleReconnect() {
33
+ if (reconnectTimer) return
34
+ reconnectTimer = setTimeout(() => {
35
+ reconnectTimer = null
36
+ if (!accessKey) return
37
+ connect(accessKey)
38
+ }, reconnectDelay)
39
+ reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY)
40
+ }
41
+
42
+ function connect(key: string) {
43
+ if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return
44
+
45
+ try {
46
+ ws = new WebSocket(getWsUrl(key))
47
+ } catch {
48
+ scheduleReconnect()
49
+ return
50
+ }
51
+
52
+ ws.onopen = () => {
53
+ connected = true
54
+ reconnectDelay = 1000
55
+ // Subscribe to all currently registered topics
56
+ const topics = Array.from(listeners.keys())
57
+ if (topics.length > 0) {
58
+ ws?.send(JSON.stringify({ type: 'subscribe', topics }))
59
+ }
60
+ }
61
+
62
+ ws.onmessage = handleMessage
63
+
64
+ ws.onclose = () => {
65
+ connected = false
66
+ ws = null
67
+ if (accessKey) scheduleReconnect()
68
+ }
69
+
70
+ ws.onerror = () => {
71
+ // onclose will fire after this
72
+ }
73
+ }
74
+
75
+ export function connectWs(key: string) {
76
+ accessKey = key
77
+ reconnectDelay = 1000
78
+ connect(key)
79
+ }
80
+
81
+ export function disconnectWs() {
82
+ accessKey = ''
83
+ if (reconnectTimer) {
84
+ clearTimeout(reconnectTimer)
85
+ reconnectTimer = null
86
+ }
87
+ if (ws) {
88
+ ws.onclose = null
89
+ ws.close()
90
+ ws = null
91
+ }
92
+ connected = false
93
+ }
94
+
95
+ export function subscribeWs(topic: string, callback: WsCallback) {
96
+ let set = listeners.get(topic)
97
+ const isNew = !set
98
+ if (!set) {
99
+ set = new Set()
100
+ listeners.set(topic, set)
101
+ }
102
+ set.add(callback)
103
+
104
+ // Tell server about new topic subscription
105
+ if (isNew && ws?.readyState === WebSocket.OPEN) {
106
+ ws.send(JSON.stringify({ type: 'subscribe', topics: [topic] }))
107
+ }
108
+ }
109
+
110
+ export function unsubscribeWs(topic: string, callback: WsCallback) {
111
+ const set = listeners.get(topic)
112
+ if (!set) return
113
+ set.delete(callback)
114
+ if (set.size === 0) {
115
+ listeners.delete(topic)
116
+ if (ws?.readyState === WebSocket.OPEN) {
117
+ ws.send(JSON.stringify({ type: 'unsubscribe', topics: [topic] }))
118
+ }
119
+ }
120
+ }
121
+
122
+ export function isWsConnected(): boolean {
123
+ return connected
124
+ }
@@ -7,7 +7,7 @@ import { speak } from '../lib/tts'
7
7
  import { getStoredAccessKey } from '../lib/api-client'
8
8
  import { useAppStore } from './use-app-store'
9
9
 
10
- interface PendingImage {
10
+ export interface PendingFile {
11
11
  file: File
12
12
  path: string
13
13
  url: string
@@ -44,8 +44,15 @@ interface ChatState {
44
44
  ttsEnabled: boolean
45
45
  toggleTts: () => void
46
46
 
47
- pendingImage: PendingImage | null
48
- setPendingImage: (img: PendingImage | null) => void
47
+ // Multi-file attachment support
48
+ pendingFiles: PendingFile[]
49
+ addPendingFile: (f: PendingFile) => void
50
+ removePendingFile: (index: number) => void
51
+ clearPendingFiles: () => void
52
+
53
+ // Legacy single-image compat (reads first pendingFile)
54
+ pendingImage: PendingFile | null
55
+ setPendingImage: (img: PendingFile | null) => void
49
56
 
50
57
  devServer: DevServerStatus | null
51
58
  setDevServer: (ds: DevServerStatus | null) => void
@@ -70,21 +77,34 @@ export const useChatStore = create<ChatState>((set, get) => ({
70
77
  lastUsage: null,
71
78
  ttsEnabled: false,
72
79
  toggleTts: () => set((s) => ({ ttsEnabled: !s.ttsEnabled })),
73
- pendingImage: null,
74
- setPendingImage: (img) => set({ pendingImage: img }),
80
+
81
+ pendingFiles: [],
82
+ addPendingFile: (f) => set((s) => ({ pendingFiles: [...s.pendingFiles, f] })),
83
+ removePendingFile: (index) => set((s) => ({ pendingFiles: s.pendingFiles.filter((_, i) => i !== index) })),
84
+ clearPendingFiles: () => set({ pendingFiles: [] }),
85
+
86
+ // Legacy compat: pendingImage reads/writes the first pending file
87
+ get pendingImage() { const files = get().pendingFiles; return files.length ? files[0] : null },
88
+ setPendingImage: (img) => set({ pendingFiles: img ? [img] : [] }),
89
+
75
90
  devServer: null,
76
91
  setDevServer: (ds) => set({ devServer: ds }),
77
92
  debugOpen: false,
78
93
  setDebugOpen: (open) => set({ debugOpen: open }),
79
94
 
80
95
  sendMessage: async (text: string) => {
81
- if (!text.trim() || get().streaming) return
96
+ const { pendingFiles } = get()
97
+ if ((!text.trim() && !pendingFiles.length) || get().streaming) return
82
98
  const sessionId = useAppStore.getState().currentSessionId
83
99
  if (!sessionId) return
84
100
 
85
- const { pendingImage } = get()
86
- const imagePath = pendingImage?.path
87
- const imageUrl = pendingImage?.url
101
+ // Primary image (backward compat)
102
+ const imagePath = pendingFiles[0]?.path
103
+ const imageUrl = pendingFiles[0]?.url
104
+ // All attached file paths
105
+ const attachedFiles = pendingFiles.length > 1
106
+ ? pendingFiles.map((f) => f.path)
107
+ : undefined
88
108
 
89
109
  const userMsg: Message = {
90
110
  role: 'user',
@@ -92,13 +112,14 @@ export const useChatStore = create<ChatState>((set, get) => ({
92
112
  time: Date.now(),
93
113
  imagePath,
94
114
  imageUrl,
115
+ attachedFiles,
95
116
  }
96
117
  set((s) => ({
97
118
  streaming: true,
98
119
  streamingSessionId: sessionId,
99
120
  streamText: '',
100
121
  messages: [...s.messages, userMsg],
101
- pendingImage: null,
122
+ pendingFiles: [],
102
123
  toolEvents: [],
103
124
  lastUsage: null,
104
125
  }))
@@ -166,7 +187,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
166
187
  } else if (event.t === 'done') {
167
188
  // done
168
189
  }
169
- })
190
+ }, attachedFiles)
170
191
 
171
192
  if (fullText.trim()) {
172
193
  const currentToolEvents = get().toolEvents
@@ -218,9 +239,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
218
239
  set({ messages: msgs })
219
240
  }
220
241
  // Re-send the last user message through the normal SSE flow
221
- // Temporarily set pendingImage if there was one
222
242
  if (imagePath) {
223
- set({ pendingImage: { file: new File([], ''), path: imagePath, url: '' } })
243
+ set({ pendingFiles: [{ file: new File([], ''), path: imagePath, url: '' }] })
224
244
  }
225
245
  await get().sendMessage(message)
226
246
  } catch {
@@ -11,6 +11,7 @@ export interface Message {
11
11
  time: number
12
12
  imagePath?: string
13
13
  imageUrl?: string
14
+ attachedFiles?: string[]
14
15
  toolEvents?: MessageToolEvent[]
15
16
  kind?: 'chat' | 'heartbeat' | 'system'
16
17
  suppressed?: boolean
@@ -22,6 +23,7 @@ export interface ProviderInfo {
22
23
  id: ProviderType
23
24
  name: string
24
25
  models: string[]
26
+ defaultModels?: string[]
25
27
  requiresApiKey: boolean
26
28
  optionalApiKey?: boolean
27
29
  requiresEndpoint: boolean
@@ -321,7 +323,7 @@ export interface MemoryEntry {
321
323
  }
322
324
 
323
325
  export type SessionType = 'human' | 'orchestrated'
324
- export type AppView = 'sessions' | 'agents' | 'schedules' | 'memory' | 'tasks' | 'secrets' | 'providers' | 'skills' | 'connectors' | 'webhooks' | 'mcp_servers' | 'knowledge' | 'plugins' | 'usage' | 'runs' | 'logs'
326
+ export type AppView = 'agents' | 'schedules' | 'memory' | 'tasks' | 'secrets' | 'providers' | 'skills' | 'connectors' | 'webhooks' | 'mcp_servers' | 'knowledge' | 'plugins' | 'usage' | 'runs' | 'logs'
325
327
 
326
328
  // --- Session Runs ---
327
329
 
@@ -588,6 +590,11 @@ export interface BoardTask {
588
590
  reasons: string[]
589
591
  checkedAt: number
590
592
  } | null
593
+ pendingApproval?: {
594
+ toolName: string
595
+ args: Record<string, unknown>
596
+ threadId: string
597
+ } | null
591
598
  }
592
599
 
593
600
  // --- MCP Servers ---
@@ -1,42 +0,0 @@
1
- import { NextResponse } from 'next/server'
2
- import { z } from 'zod'
3
- import { buildLLM } from '@/lib/server/build-llm'
4
-
5
- const agentSchema = z.object({
6
- name: z.string().describe('Short name for the agent'),
7
- description: z.string().describe('One sentence describing what it does'),
8
- systemPrompt: z.string().describe('Full system prompt — thorough and specific, at least 3-4 paragraphs'),
9
- isOrchestrator: z.boolean().describe('True only if it needs to coordinate multiple sub-agents'),
10
- })
11
-
12
- const GENERATE_PROMPT = `You are a agent generator. The user will describe an AI agent they want to create. Generate a complete agent definition.
13
-
14
- Set isOrchestrator to true ONLY if the user describes something that needs to coordinate multiple sub-agents.
15
- Make the systemPrompt detailed and actionable — at least 3-4 paragraphs. Include specific instructions about how the agent should behave, what it should focus on, and how it should format its responses.`
16
-
17
- export async function POST(req: Request) {
18
- const { prompt } = await req.json()
19
- if (!prompt?.trim()) {
20
- return NextResponse.json({ error: 'prompt is required' }, { status: 400 })
21
- }
22
-
23
- try {
24
- const { llm, provider } = await buildLLM()
25
- const structured = provider === 'anthropic'
26
- ? llm.withStructuredOutput(agentSchema)
27
- : (llm as any).withStructuredOutput(agentSchema, {
28
- name: 'agent_definition',
29
- method: 'functionCalling',
30
- })
31
- const result = await structured.invoke([
32
- { role: 'system' as const, content: GENERATE_PROMPT },
33
- { role: 'user' as const, content: prompt },
34
- ], { signal: AbortSignal.timeout(60_000) })
35
-
36
- return NextResponse.json(result)
37
- } catch (err: unknown) {
38
- const message = err instanceof Error ? err.message : 'Generation failed'
39
- console.error('[agent-generate] Error:', message)
40
- return NextResponse.json({ error: message }, { status: 500 })
41
- }
42
- }
@@ -1,12 +0,0 @@
1
- import { NextResponse } from 'next/server'
2
- import { buildLLM } from '@/lib/server/build-llm'
3
-
4
- /** Returns which provider/model the generate endpoints will use */
5
- export async function GET() {
6
- try {
7
- const { provider, model } = await buildLLM()
8
- return NextResponse.json({ provider, model })
9
- } catch (err: any) {
10
- return NextResponse.json({ provider: 'none', model: '', error: err.message })
11
- }
12
- }
@@ -1,106 +0,0 @@
1
- import { NextResponse } from 'next/server'
2
- import { z } from 'zod'
3
- import { buildLLM } from '@/lib/server/build-llm'
4
-
5
- const scheduleSchema = z.object({
6
- name: z.string().describe('Short descriptive name for the schedule'),
7
- taskPrompt: z.string().describe('The prompt/instructions the agent should execute when the schedule fires'),
8
- scheduleType: z.enum(['cron', 'interval', 'once']).describe('Type of schedule'),
9
- cron: z.string().optional().describe('Cron expression if scheduleType is cron, e.g. "0 9 * * 1" for every Monday at 9am'),
10
- intervalMs: z.number().optional().describe('Interval in milliseconds if scheduleType is interval'),
11
- })
12
-
13
- const taskSchema = z.object({
14
- title: z.string().describe('Short descriptive title for the task'),
15
- description: z.string().describe('Detailed description of what needs to be done'),
16
- })
17
-
18
- const skillSchema = z.object({
19
- name: z.string().describe('Short name for the skill'),
20
- description: z.string().describe('One sentence describing what this skill does'),
21
- content: z.string().describe('Full markdown content of the skill — detailed instructions, at least 3-4 paragraphs'),
22
- })
23
-
24
- const providerSchema = z.object({
25
- name: z.string().describe('Display name for the provider, e.g. "Together AI", "Groq", "z.ai"'),
26
- baseUrl: z.string().describe('The OpenAI-compatible API base URL, e.g. "https://api.together.xyz/v1" or "https://api.groq.com/openai/v1"'),
27
- models: z.string().describe('Comma-separated list of available model IDs, e.g. "llama-3-70b,mixtral-8x7b"'),
28
- requiresApiKey: z.boolean().describe('Whether this provider requires an API key'),
29
- })
30
-
31
- const SCHEMAS: Record<string, { schema: z.ZodObject<any>; prompt: string }> = {
32
- schedule: {
33
- schema: scheduleSchema,
34
- prompt: `You are a schedule generator for SwarmClaw, an AI agent orchestration platform. The user will describe what they want scheduled. Generate a complete schedule definition.
35
-
36
- Choose the appropriate scheduleType:
37
- - "cron" for recurring schedules (provide a cron expression)
38
- - "interval" for periodic execution (provide intervalMs in milliseconds)
39
- - "once" for one-time execution
40
-
41
- Make the taskPrompt detailed and specific — it should contain everything the agent needs to know to complete the task.`,
42
- },
43
- task: {
44
- schema: taskSchema,
45
- prompt: `You are a task generator for SwarmClaw, an AI agent orchestration platform. The user will describe a task they want to create. Generate a clear task definition.
46
-
47
- Make the description thorough and actionable — include specific goals, acceptance criteria, and any relevant context. The description should give an AI agent enough information to complete the task autonomously.`,
48
- },
49
- skill: {
50
- schema: skillSchema,
51
- prompt: `You are a skill generator for SwarmClaw, an AI agent orchestration platform. Skills are reusable markdown instruction sets that get injected into agent system prompts.
52
-
53
- The user will describe what skill they want. Generate a comprehensive skill definition with detailed markdown content. The content should be:
54
- - Written as instructions/guidelines for an AI agent
55
- - Thorough and specific (at least 3-4 paragraphs)
56
- - Structured with markdown headings, lists, and examples
57
- - Focused on actionable guidance the agent can follow`,
58
- },
59
- provider: {
60
- schema: providerSchema,
61
- prompt: `You are a provider configuration generator for SwarmClaw, an AI agent orchestration platform. The user will name an LLM provider they want to add.
62
-
63
- Generate the correct OpenAI-compatible API configuration. You should know the base URLs and model names for popular providers:
64
- - Together AI: https://api.together.xyz/v1
65
- - Groq: https://api.groq.com/openai/v1
66
- - Fireworks: https://api.fireworks.ai/inference/v1
67
- - Perplexity: https://api.perplexity.ai
68
- - Mistral: https://api.mistral.ai/v1
69
- - DeepSeek: https://api.deepseek.com/v1
70
- - OpenRouter: https://openrouter.ai/api/v1
71
- - xAI/Grok: https://api.x.ai/v1
72
- - z.ai: https://api.z.ai/v1
73
-
74
- List the most popular/recommended models for the provider as comma-separated values. Most providers require an API key.
75
- If you don't know the exact URL, make your best guess based on common patterns (provider domain + /v1).`,
76
- },
77
- }
78
-
79
- export async function POST(req: Request) {
80
- const { type, prompt } = await req.json()
81
- if (!prompt?.trim()) {
82
- return NextResponse.json({ error: 'prompt is required' }, { status: 400 })
83
- }
84
- const config = SCHEMAS[type]
85
- if (!config) {
86
- return NextResponse.json({ error: `Invalid type: ${type}. Must be one of: ${Object.keys(SCHEMAS).join(', ')}` }, { status: 400 })
87
- }
88
-
89
- try {
90
- const { llm, provider } = await buildLLM()
91
- const structured = provider === 'anthropic'
92
- ? llm.withStructuredOutput(config.schema)
93
- : (llm as any).withStructuredOutput(config.schema, {
94
- name: `${type}_definition`,
95
- method: 'functionCalling',
96
- })
97
- const result = await structured.invoke([
98
- { role: 'system' as const, content: config.prompt },
99
- { role: 'user' as const, content: prompt },
100
- ], { signal: AbortSignal.timeout(60_000) })
101
- return NextResponse.json(result)
102
- } catch (err: unknown) {
103
- const message = err instanceof Error ? err.message : 'Generation failed'
104
- return NextResponse.json({ error: message }, { status: 500 })
105
- }
106
- }
Binary file
@@ -1,77 +0,0 @@
1
- 'use client'
2
-
3
- import { useState, useEffect } from 'react'
4
- import { api } from '@/lib/api-client'
5
-
6
- interface Props {
7
- aiPrompt: string
8
- setAiPrompt: (v: string) => void
9
- generating: boolean
10
- generated: boolean
11
- genError: string
12
- onGenerate: () => void
13
- appSettings?: Record<string, any>
14
- placeholder: string
15
- }
16
-
17
- export function AiGenBlock({ aiPrompt, setAiPrompt, generating, generated, genError, onGenerate, placeholder }: Props) {
18
- const [expanded, setExpanded] = useState(false)
19
- const [genInfo, setGenInfo] = useState<{ provider: string; model: string } | null>(null)
20
-
21
- useEffect(() => {
22
- if (expanded && !genInfo) {
23
- api<{ provider: string; model: string }>('GET', '/generate/info')
24
- .then(setGenInfo)
25
- .catch((err) => console.error('AI gen info fetch failed:', err))
26
- }
27
- }, [expanded])
28
-
29
- return (
30
- <div className="mb-10">
31
- <button
32
- onClick={() => setExpanded(!expanded)}
33
- className="flex items-center gap-2.5 px-4 py-3 rounded-[14px] border border-[#6366F1]/15 bg-[#6366F1]/[0.03] hover:bg-[#6366F1]/[0.06] transition-all cursor-pointer w-full text-left"
34
- style={{ fontFamily: 'inherit' }}
35
- >
36
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" className="text-accent-bright shrink-0">
37
- <path d="M12 2L14.5 9.5L22 12L14.5 14.5L12 22L9.5 14.5L2 12L9.5 9.5L12 2Z" fill="currentColor" />
38
- </svg>
39
- <span className="font-display text-[13px] font-600 text-accent-bright flex-1">Generate with AI</span>
40
- <svg
41
- width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"
42
- className={`text-accent-bright/50 transition-transform duration-200 ${expanded ? 'rotate-180' : ''}`}
43
- >
44
- <polyline points="6 9 12 15 18 9" />
45
- </svg>
46
- </button>
47
-
48
- {expanded && (
49
- <div className="mt-3 p-5 rounded-[18px] border border-[#6366F1]/15 bg-[#6366F1]/[0.03]"
50
- style={{ animation: 'fade-in 0.2s ease' }}>
51
- <textarea
52
- value={aiPrompt}
53
- onChange={(e) => setAiPrompt(e.target.value)}
54
- placeholder={placeholder}
55
- rows={2}
56
- className="w-full px-4 py-3 rounded-[12px] border border-[#6366F1]/10 bg-[#6366F1]/[0.02] text-text text-[14px] outline-none transition-all duration-200 placeholder:text-text-3/70 focus:border-[#6366F1]/30 resize-none"
57
- style={{ fontFamily: 'inherit' }}
58
- autoFocus
59
- />
60
- <button
61
- onClick={onGenerate}
62
- disabled={generating || !aiPrompt.trim()}
63
- className="mt-3 px-5 py-2.5 rounded-[12px] border-none bg-[#6366F1] text-white text-[13px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110 active:scale-[0.97] shadow-[0_2px_12px_rgba(99,102,241,0.2)]"
64
- style={{ fontFamily: 'inherit' }}
65
- >
66
- {generating ? 'Generating...' : generated ? 'Regenerate' : 'Generate'}
67
- </button>
68
- {generated && <span className="ml-3 text-[12px] text-emerald-400/70">Fields populated — edit below</span>}
69
- {genError && <p className="mt-2 text-[12px] text-red-400/80">{genError}</p>}
70
- <p className="mt-3 text-[11px] text-text-3/50">
71
- Using {genInfo ? `${genInfo.model} via ${genInfo.provider}` : 'auto-detected provider'}
72
- </p>
73
- </div>
74
- )}
75
- </div>
76
- )
77
- }