@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
@@ -0,0 +1,274 @@
1
+ import { BaseCheckpointSaver } from '@langchain/langgraph'
2
+ import type { RunnableConfig } from '@langchain/core/runnables'
3
+ import type {
4
+ Checkpoint,
5
+ CheckpointTuple,
6
+ CheckpointListOptions,
7
+ } from '@langchain/langgraph-checkpoint'
8
+ import type {
9
+ CheckpointMetadata,
10
+ PendingWrite,
11
+ CheckpointPendingWrite,
12
+ } from '@langchain/langgraph-checkpoint'
13
+ import Database from 'better-sqlite3'
14
+ import path from 'path'
15
+ import { DATA_DIR } from './data-dir'
16
+
17
+ const DB_PATH = path.join(DATA_DIR, 'swarmclaw.db')
18
+
19
+ function getDb(): Database.Database {
20
+ const db = new Database(DB_PATH)
21
+ db.pragma('journal_mode = WAL')
22
+ return db
23
+ }
24
+
25
+ // Ensure tables exist
26
+ const initDb = getDb()
27
+ initDb.exec(`
28
+ CREATE TABLE IF NOT EXISTS langgraph_checkpoints (
29
+ thread_id TEXT NOT NULL,
30
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
31
+ checkpoint_id TEXT NOT NULL,
32
+ parent_checkpoint_id TEXT,
33
+ type TEXT NOT NULL DEFAULT 'json',
34
+ checkpoint BLOB NOT NULL,
35
+ metadata BLOB NOT NULL DEFAULT '{}',
36
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
37
+ PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
38
+ )
39
+ `)
40
+ initDb.exec(`
41
+ CREATE TABLE IF NOT EXISTS langgraph_writes (
42
+ thread_id TEXT NOT NULL,
43
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
44
+ checkpoint_id TEXT NOT NULL,
45
+ task_id TEXT NOT NULL,
46
+ idx INTEGER NOT NULL,
47
+ channel TEXT NOT NULL,
48
+ type TEXT NOT NULL DEFAULT 'json',
49
+ value BLOB,
50
+ PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
51
+ )
52
+ `)
53
+ initDb.close()
54
+
55
+ function getThreadId(config: RunnableConfig): string {
56
+ return (config.configurable?.thread_id as string) || ''
57
+ }
58
+
59
+ function getCheckpointNs(config: RunnableConfig): string {
60
+ return (config.configurable?.checkpoint_ns as string) || ''
61
+ }
62
+
63
+ function getCheckpointId(config: RunnableConfig): string | undefined {
64
+ return config.configurable?.checkpoint_id as string | undefined
65
+ }
66
+
67
+ export class SqliteCheckpointSaver extends BaseCheckpointSaver {
68
+ private db: Database.Database
69
+
70
+ constructor() {
71
+ super()
72
+ this.db = getDb()
73
+ }
74
+
75
+ async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {
76
+ const threadId = getThreadId(config)
77
+ if (!threadId) return undefined
78
+
79
+ const ns = getCheckpointNs(config)
80
+ const checkpointId = getCheckpointId(config)
81
+
82
+ let row: any
83
+ if (checkpointId) {
84
+ row = this.db.prepare(
85
+ `SELECT * FROM langgraph_checkpoints WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?`
86
+ ).get(threadId, ns, checkpointId)
87
+ } else {
88
+ row = this.db.prepare(
89
+ `SELECT * FROM langgraph_checkpoints WHERE thread_id = ? AND checkpoint_ns = ? ORDER BY created_at DESC LIMIT 1`
90
+ ).get(threadId, ns)
91
+ }
92
+
93
+ if (!row) return undefined
94
+
95
+ const checkpoint = JSON.parse(
96
+ typeof row.checkpoint === 'string' ? row.checkpoint : Buffer.from(row.checkpoint).toString()
97
+ ) as Checkpoint
98
+ const metadata = JSON.parse(
99
+ typeof row.metadata === 'string' ? row.metadata : Buffer.from(row.metadata).toString()
100
+ ) as CheckpointMetadata
101
+
102
+ const resultConfig: RunnableConfig = {
103
+ configurable: {
104
+ thread_id: threadId,
105
+ checkpoint_ns: ns,
106
+ checkpoint_id: row.checkpoint_id,
107
+ },
108
+ }
109
+
110
+ const parentConfig = row.parent_checkpoint_id
111
+ ? {
112
+ configurable: {
113
+ thread_id: threadId,
114
+ checkpoint_ns: ns,
115
+ checkpoint_id: row.parent_checkpoint_id,
116
+ },
117
+ }
118
+ : undefined
119
+
120
+ // Load pending writes
121
+ const writeRows = this.db.prepare(
122
+ `SELECT task_id, channel, value FROM langgraph_writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY idx`
123
+ ).all(threadId, ns, row.checkpoint_id) as any[]
124
+
125
+ const pendingWrites: CheckpointPendingWrite[] = writeRows.map((w) => [
126
+ w.task_id,
127
+ w.channel,
128
+ w.value ? JSON.parse(typeof w.value === 'string' ? w.value : Buffer.from(w.value).toString()) : undefined,
129
+ ])
130
+
131
+ return {
132
+ config: resultConfig,
133
+ checkpoint,
134
+ metadata,
135
+ parentConfig,
136
+ pendingWrites,
137
+ }
138
+ }
139
+
140
+ async *list(
141
+ config: RunnableConfig,
142
+ options?: CheckpointListOptions,
143
+ ): AsyncGenerator<CheckpointTuple> {
144
+ const threadId = getThreadId(config)
145
+ if (!threadId) return
146
+
147
+ const ns = getCheckpointNs(config)
148
+ const limit = options?.limit ?? 100
149
+
150
+ let query = `SELECT * FROM langgraph_checkpoints WHERE thread_id = ? AND checkpoint_ns = ?`
151
+ const params: any[] = [threadId, ns]
152
+
153
+ if (options?.before?.configurable?.checkpoint_id) {
154
+ query += ` AND checkpoint_id < ?`
155
+ params.push(options.before.configurable.checkpoint_id)
156
+ }
157
+
158
+ query += ` ORDER BY created_at DESC LIMIT ?`
159
+ params.push(limit)
160
+
161
+ const rows = this.db.prepare(query).all(...params) as any[]
162
+
163
+ for (const row of rows) {
164
+ const checkpoint = JSON.parse(
165
+ typeof row.checkpoint === 'string' ? row.checkpoint : Buffer.from(row.checkpoint).toString()
166
+ ) as Checkpoint
167
+ const metadata = JSON.parse(
168
+ typeof row.metadata === 'string' ? row.metadata : Buffer.from(row.metadata).toString()
169
+ ) as CheckpointMetadata
170
+
171
+ const resultConfig: RunnableConfig = {
172
+ configurable: {
173
+ thread_id: threadId,
174
+ checkpoint_ns: ns,
175
+ checkpoint_id: row.checkpoint_id,
176
+ },
177
+ }
178
+
179
+ const parentConfig = row.parent_checkpoint_id
180
+ ? {
181
+ configurable: {
182
+ thread_id: threadId,
183
+ checkpoint_ns: ns,
184
+ checkpoint_id: row.parent_checkpoint_id,
185
+ },
186
+ }
187
+ : undefined
188
+
189
+ yield {
190
+ config: resultConfig,
191
+ checkpoint,
192
+ metadata,
193
+ parentConfig,
194
+ }
195
+ }
196
+ }
197
+
198
+ async put(
199
+ config: RunnableConfig,
200
+ checkpoint: Checkpoint,
201
+ metadata: CheckpointMetadata,
202
+ _newVersions: Record<string, number | string>,
203
+ ): Promise<RunnableConfig> {
204
+ const threadId = getThreadId(config)
205
+ const ns = getCheckpointNs(config)
206
+ const parentId = getCheckpointId(config)
207
+
208
+ this.db.prepare(`
209
+ INSERT OR REPLACE INTO langgraph_checkpoints
210
+ (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, checkpoint, metadata, created_at)
211
+ VALUES (?, ?, ?, ?, ?, ?, ?)
212
+ `).run(
213
+ threadId,
214
+ ns,
215
+ checkpoint.id,
216
+ parentId || null,
217
+ JSON.stringify(checkpoint),
218
+ JSON.stringify(metadata),
219
+ Date.now(),
220
+ )
221
+
222
+ return {
223
+ configurable: {
224
+ thread_id: threadId,
225
+ checkpoint_ns: ns,
226
+ checkpoint_id: checkpoint.id,
227
+ },
228
+ }
229
+ }
230
+
231
+ async putWrites(
232
+ config: RunnableConfig,
233
+ writes: PendingWrite[],
234
+ taskId: string,
235
+ ): Promise<void> {
236
+ const threadId = getThreadId(config)
237
+ const ns = getCheckpointNs(config)
238
+ const checkpointId = getCheckpointId(config)
239
+ if (!checkpointId) return
240
+
241
+ const stmt = this.db.prepare(`
242
+ INSERT OR REPLACE INTO langgraph_writes
243
+ (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, value)
244
+ VALUES (?, ?, ?, ?, ?, ?, ?)
245
+ `)
246
+
247
+ const insertMany = this.db.transaction((items: PendingWrite[]) => {
248
+ items.forEach(([channel, value], idx) => {
249
+ stmt.run(
250
+ threadId,
251
+ ns,
252
+ checkpointId,
253
+ taskId,
254
+ idx,
255
+ channel as string,
256
+ value !== undefined ? JSON.stringify(value) : null,
257
+ )
258
+ })
259
+ })
260
+
261
+ insertMany(writes)
262
+ }
263
+
264
+ async deleteThread(threadId: string): Promise<void> {
265
+ this.db.prepare(`DELETE FROM langgraph_checkpoints WHERE thread_id = ?`).run(threadId)
266
+ this.db.prepare(`DELETE FROM langgraph_writes WHERE thread_id = ?`).run(threadId)
267
+ }
268
+ }
269
+
270
+ let _saver: SqliteCheckpointSaver | undefined
271
+ export function getCheckpointSaver(): SqliteCheckpointSaver {
272
+ if (!_saver) _saver = new SqliteCheckpointSaver()
273
+ return _saver
274
+ }
@@ -1,6 +1,7 @@
1
1
  import crypto from 'crypto'
2
+ import { z } from 'zod'
2
3
  import type { GoalContract, MessageToolEvent } from '@/types'
3
- import { loadSessions, saveSessions, loadTasks, saveTasks } from './storage'
4
+ import { loadSessions, saveSessions, loadAgents, saveAgents, loadTasks, saveTasks } from './storage'
4
5
  import { log } from './logger'
5
6
  import { getMemoryDb } from './memory-db'
6
7
  import {
@@ -18,6 +19,7 @@ const MEMORY_NOTE_MIN_INTERVAL_MS = 90 * 60 * 1000
18
19
  const DEFAULT_FOLLOWUP_DELAY_SEC = 45
19
20
  const MAX_FOLLOWUP_CHAIN = 6
20
21
  const META_LINE_RE = /\[MAIN_LOOP_META\]\s*(\{[^\n]*\})/i
22
+ const AGENT_HEARTBEAT_META_RE = /\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i
21
23
  const SCREENSHOT_GOAL_HINT = /\b(screenshot|screen shot|snapshot|capture)\b/i
22
24
  const DELIVERY_GOAL_HINT = /\b(send|deliver|return|share|upload|post|message)\b/i
23
25
  const SCHEDULE_GOAL_HINT = /\b(schedule|scheduled|every\s+\w+|interval|cron|recurr)\b/i
@@ -822,10 +824,67 @@ export function pushMainLoopEventToMainSessions(input: PushMainLoopEventInput):
822
824
  return changed
823
825
  }
824
826
 
827
+ const AgentHeartbeatMetaSchema = z.object({
828
+ goal: z.string().trim().optional(),
829
+ status: z.enum(['progress', 'ok', 'idle', 'blocked']).optional(),
830
+ next_action: z.string().trim().optional(),
831
+ }).passthrough()
832
+
833
+ type AgentHeartbeatMeta = z.infer<typeof AgentHeartbeatMetaSchema>
834
+
835
+ function parseAgentHeartbeatMeta(text: string): AgentHeartbeatMeta | null {
836
+ const raw = (text || '').trim()
837
+ if (!raw) return null
838
+ const match = raw.match(AGENT_HEARTBEAT_META_RE)
839
+ if (!match?.[1]) return null
840
+ try {
841
+ const parsed = JSON.parse(match[1])
842
+ return AgentHeartbeatMetaSchema.parse(parsed)
843
+ } catch {
844
+ return null
845
+ }
846
+ }
847
+
848
+ function handleAgentHeartbeatResult(session: any, input: HandleMainLoopRunResultInput): null {
849
+ if (!input.internal || input.source !== 'heartbeat') return null
850
+ if (!session.agentId) return null
851
+ const text = input.resultText || ''
852
+ if (!text.trim()) return null
853
+
854
+ const meta = parseAgentHeartbeatMeta(text)
855
+ if (!meta) return null
856
+
857
+ const agents = loadAgents()
858
+ const agent = agents[session.agentId]
859
+ if (!agent) return null
860
+
861
+ let changed = false
862
+ if (meta.goal && meta.goal !== agent.heartbeatGoal) {
863
+ agent.heartbeatGoal = meta.goal
864
+ changed = true
865
+ log.info('agent-heartbeat', `Goal updated for agent ${agent.name}: ${meta.goal.slice(0, 120)}`)
866
+ }
867
+ if (meta.next_action) {
868
+ agent.heartbeatNextAction = meta.next_action
869
+ changed = true
870
+ }
871
+ if (meta.status) {
872
+ agent.heartbeatStatus = meta.status
873
+ changed = true
874
+ }
875
+
876
+ if (changed) {
877
+ agents[session.agentId] = agent
878
+ saveAgents(agents)
879
+ }
880
+ return null
881
+ }
882
+
825
883
  export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): MainLoopFollowupRequest | null {
826
884
  const sessions = loadSessions()
827
885
  const session = sessions[input.sessionId]
828
- if (!session || !isMainSession(session)) return null
886
+ if (!session) return null
887
+ if (!isMainSession(session)) return handleAgentHeartbeatResult(session, input)
829
888
 
830
889
  const now = Date.now()
831
890
  const state = normalizeState(session.mainLoopState, now)