@swarmclawai/swarmclaw 0.7.1 → 0.7.3

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 (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -0,0 +1,13 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { buildConnectorDoctorPreview, buildConnectorDoctorReport, type ConnectorDoctorPreviewInput } from '@/lib/server/connectors/doctor'
3
+ import { loadConnectors } from '@/lib/server/storage'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ export async function POST(req: Request) {
8
+ const body = await req.json().catch(() => ({})) as ConnectorDoctorPreviewInput
9
+ const connectors = loadConnectors()
10
+ const baseConnector = typeof body.id === 'string' ? connectors[body.id] : null
11
+ const connector = buildConnectorDoctorPreview({ baseConnector, input: body })
12
+ return NextResponse.json(buildConnectorDoctorReport(connector, body.sampleMsg, { baseConnector }))
13
+ }
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { exec } from 'child_process'
2
+ import { spawn } from 'child_process'
3
3
  import fs from 'fs'
4
4
  import path from 'path'
5
5
 
@@ -19,25 +19,27 @@ export async function POST(req: Request) {
19
19
  const isDir = fs.statSync(resolved).isDirectory()
20
20
  const platform = process.platform
21
21
 
22
- // Determine the command to reveal in the OS file manager
23
- let cmd: string
22
+ let command: string
23
+ let args: string[]
24
24
  if (platform === 'darwin') {
25
- // macOS: -R reveals in Finder (selects the item), for dirs just open the dir
26
- cmd = isDir ? `open "${resolved}"` : `open -R "${resolved}"`
25
+ command = 'open'
26
+ args = isDir ? [resolved] : ['-R', resolved]
27
27
  } else if (platform === 'win32') {
28
- cmd = isDir ? `explorer "${resolved}"` : `explorer /select,"${resolved}"`
28
+ command = 'explorer'
29
+ args = isDir ? [resolved] : [`/select,${resolved}`]
29
30
  } else {
30
- // Linux: xdg-open on the directory containing the file
31
- cmd = `xdg-open "${isDir ? resolved : path.dirname(resolved)}"`
31
+ command = 'xdg-open'
32
+ args = [isDir ? resolved : path.dirname(resolved)]
32
33
  }
33
34
 
34
35
  return new Promise<NextResponse>((resolve) => {
35
- exec(cmd, (err) => {
36
- if (err) {
37
- resolve(NextResponse.json({ error: err.message }, { status: 500 }))
38
- } else {
39
- resolve(NextResponse.json({ ok: true }))
40
- }
36
+ const child = spawn(command, args, { stdio: 'ignore' })
37
+ child.once('error', (err) => {
38
+ resolve(NextResponse.json({ error: err.message }, { status: 500 }))
39
+ })
40
+ child.once('spawn', () => {
41
+ child.unref()
42
+ resolve(NextResponse.json({ ok: true }))
41
43
  })
42
44
  })
43
45
  }
@@ -1,6 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import path from 'path'
2
3
  import { getMemoryDb } from '@/lib/server/memory-db'
3
4
  import { loadSettings } from '@/lib/server/storage'
5
+ import { syncAllSessionArchiveMemories } from '@/lib/server/session-archive-memory'
6
+ import { DATA_DIR } from '@/lib/server/data-dir'
4
7
 
5
8
  function parseBool(value: unknown, fallback: boolean): boolean {
6
9
  if (typeof value === 'boolean') return value
@@ -33,10 +36,13 @@ export async function GET(req: Request) {
33
36
  24 * 365,
34
37
  )
35
38
  const analyzed = db.analyzeMaintenance(ttlHours)
39
+ const archiveSync = syncAllSessionArchiveMemories()
36
40
  return NextResponse.json({
37
41
  ok: true,
38
42
  ttlHours,
39
43
  analyzed,
44
+ archiveSync,
45
+ archiveExportDir: path.join(DATA_DIR, 'session-archives'),
40
46
  })
41
47
  }
42
48
 
@@ -46,6 +52,9 @@ export async function POST(req: Request) {
46
52
  const db = getMemoryDb()
47
53
  const ttlHours = parseIntBounded(body?.ttlHours ?? settings.memoryWorkingTtlHours, 24, 1, 24 * 365)
48
54
  const maxDeletes = parseIntBounded(body?.maxDeletes, 500, 1, 20_000)
55
+ const archiveSync = body?.syncArchives === false
56
+ ? { synced: 0, skipped: 0, sessionIds: [] }
57
+ : syncAllSessionArchiveMemories()
49
58
  const result = db.maintain({
50
59
  ttlHours,
51
60
  maxDeletes,
@@ -57,7 +66,8 @@ export async function POST(req: Request) {
57
66
  ok: true,
58
67
  ttlHours,
59
68
  maxDeletes,
69
+ archiveSync,
70
+ archiveExportDir: path.join(DATA_DIR, 'session-archives'),
60
71
  ...result,
61
72
  })
62
73
  }
63
-
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+ import { resolveOpenClawGatewayAgentId } from '@/lib/server/openclaw-agent-resolver'
3
4
 
4
5
  const AGENT_FILES = ['SOUL.md', 'IDENTITY.md', 'USER.md', 'TOOLS.md', 'HEARTBEAT.md', 'MEMORY.md', 'AGENTS.md'] as const
5
6
 
@@ -16,12 +17,24 @@ export async function GET(req: Request) {
16
17
  return NextResponse.json({ error: 'OpenClaw gateway not connected' }, { status: 503 })
17
18
  }
18
19
 
20
+ let gatewayAgentId: string
21
+ try {
22
+ gatewayAgentId = await resolveOpenClawGatewayAgentId(agentId, gw)
23
+ } catch (err: unknown) {
24
+ const message = err instanceof Error ? err.message : String(err)
25
+ const status = message.includes('not an OpenClaw agent') ? 400 : 404
26
+ return NextResponse.json({ error: message }, { status })
27
+ }
28
+
19
29
  const files: Record<string, { content: string; error?: string }> = {}
20
30
  await Promise.all(
21
31
  AGENT_FILES.map(async (filename) => {
22
32
  try {
23
- const result = await gw.rpc('agents.files.get', { agentId, filename }) as { content?: string } | undefined
24
- files[filename] = { content: result?.content ?? '' }
33
+ const result = await gw.rpc('agents.files.get', {
34
+ agentId: gatewayAgentId,
35
+ name: filename,
36
+ }) as { file?: { content?: string } } | undefined
37
+ files[filename] = { content: result?.file?.content ?? '' }
25
38
  } catch (err: unknown) {
26
39
  files[filename] = { content: '', error: err instanceof Error ? err.message : String(err) }
27
40
  }
@@ -48,10 +61,20 @@ export async function PUT(req: Request) {
48
61
  }
49
62
 
50
63
  try {
51
- await gw.rpc('agents.files.set', { agentId, filename, content: content ?? '' })
64
+ const gatewayAgentId = await resolveOpenClawGatewayAgentId(agentId, gw)
65
+ await gw.rpc('agents.files.set', {
66
+ agentId: gatewayAgentId,
67
+ name: filename,
68
+ content: content ?? '',
69
+ })
52
70
  return NextResponse.json({ ok: true })
53
71
  } catch (err: unknown) {
54
72
  const message = err instanceof Error ? err.message : String(err)
55
- return NextResponse.json({ error: message }, { status: 502 })
73
+ const status = message.includes('not an OpenClaw agent')
74
+ ? 400
75
+ : message.includes('not found')
76
+ ? 404
77
+ : 502
78
+ return NextResponse.json({ error: message }, { status })
56
79
  }
57
80
  }
@@ -1,5 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+ import { resolveOpenClawGatewayAgentId } from '@/lib/server/openclaw-agent-resolver'
4
+ import { normalizeOpenClawSkillsPayload } from '@/lib/server/openclaw-skills-normalize'
3
5
  import { loadAgents, saveAgents } from '@/lib/server/storage'
4
6
  import { notify } from '@/lib/server/ws-hub'
5
7
  import type { OpenClawSkillEntry, SkillAllowlistMode } from '@/types'
@@ -18,11 +20,17 @@ export async function GET(req: Request) {
18
20
  }
19
21
 
20
22
  try {
21
- const result = await gw.rpc('skills.status', { agentId }) as OpenClawSkillEntry[] | undefined
22
- return NextResponse.json(result ?? [])
23
+ const gatewayAgentId = await resolveOpenClawGatewayAgentId(agentId, gw)
24
+ const result = await gw.rpc('skills.status', { agentId: gatewayAgentId }) as unknown
25
+ return NextResponse.json(normalizeOpenClawSkillsPayload(result))
23
26
  } catch (err: unknown) {
24
27
  const message = err instanceof Error ? err.message : String(err)
25
- return NextResponse.json({ error: message }, { status: 502 })
28
+ const status = message.includes('not an OpenClaw agent')
29
+ ? 400
30
+ : message.includes('not found')
31
+ ? 404
32
+ : 502
33
+ return NextResponse.json({ error: message }, { status })
26
34
  }
27
35
  }
28
36
 
@@ -0,0 +1,24 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getPluginManager } from '@/lib/server/plugins'
3
+
4
+ export async function POST(req: Request) {
5
+ const body = await req.json()
6
+ const filename = typeof body?.filename === 'string' ? body.filename : ''
7
+ const packageManager = typeof body?.packageManager === 'string' ? body.packageManager : undefined
8
+
9
+ if (!filename) {
10
+ return NextResponse.json({ error: 'filename is required' }, { status: 400 })
11
+ }
12
+
13
+ try {
14
+ const result = await getPluginManager().installPluginDependencies(filename, {
15
+ packageManager: packageManager as import('@/types').PluginPackageManager | undefined,
16
+ })
17
+ return NextResponse.json({ ok: true, dependencyInfo: result })
18
+ } catch (err: unknown) {
19
+ return NextResponse.json(
20
+ { error: err instanceof Error ? err.message : String(err) },
21
+ { status: 400 },
22
+ )
23
+ }
24
+ }
@@ -1,110 +1,33 @@
1
1
  import { NextResponse } from 'next/server'
2
- import fs from 'fs'
3
- import path from 'path'
4
- import { getPluginManager } from '@/lib/server/plugins'
5
-
6
- const PLUGINS_DIR = path.join(process.cwd(), 'data', 'plugins')
7
-
8
- function toRawUrl(url: string): string {
9
- if (url.includes('github.com') && url.includes('/blob/')) {
10
- return url.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/')
11
- }
12
- if (url.includes('gist.github.com')) {
13
- return url.endsWith('/raw') ? url : `${url}/raw`
14
- }
15
- return url
16
- }
17
-
18
- function normalizeMarketplaceUrl(url: string): string {
19
- const trimmed = typeof url === 'string' ? url.trim() : ''
20
- if (!trimmed) return trimmed
21
-
22
- let normalized = trimmed
23
- .replace('github.com/swarmclawai/plugins/', 'github.com/swarmclawai/swarmforge/')
24
- .replace('raw.githubusercontent.com/swarmclawai/plugins/', 'raw.githubusercontent.com/swarmclawai/swarmforge/')
25
-
26
- normalized = toRawUrl(normalized)
27
-
28
- // Legacy registry entries used master and old repo names.
29
- normalized = normalized
30
- .replace('/swarmclawai/swarmforge/master/', '/swarmclawai/swarmforge/main/')
31
- .replace('/swarmclawai/plugins/master/', '/swarmclawai/swarmforge/main/')
32
- .replace('/swarmclawai/plugins/main/', '/swarmclawai/swarmforge/main/')
33
-
34
- return normalized
35
- }
2
+ import { getPluginManager, sanitizePluginFilename } from '@/lib/server/plugins'
36
3
 
37
4
  export async function POST(req: Request) {
38
5
  const body = await req.json()
39
- const { url, filename } = body
40
- const rawUrl = normalizeMarketplaceUrl(url)
6
+ const url = typeof body?.url === 'string' ? body.url : ''
7
+ const filename = typeof body?.filename === 'string' ? body.filename : ''
41
8
 
42
- // Validate URL
43
- if (!url || typeof url !== 'string' || !url.startsWith('https://')) {
9
+ if (!url || !url.startsWith('https://')) {
44
10
  return NextResponse.json(
45
11
  { error: 'URL must be a valid HTTPS URL' },
46
12
  { status: 400 },
47
13
  )
48
14
  }
49
15
 
50
- // Validate filename
51
- if (!filename || typeof filename !== 'string' || !filename.endsWith('.js')) {
52
- return NextResponse.json(
53
- { error: 'Filename must end in .js' },
54
- { status: 400 },
55
- )
56
- }
57
-
58
- // Path traversal protection
59
- const sanitized = path.basename(filename)
60
- if (sanitized !== filename || filename.includes('..')) {
61
- return NextResponse.json(
62
- { error: 'Invalid filename' },
63
- { status: 400 },
64
- )
65
- }
66
-
67
16
  try {
68
- const res = await fetch(rawUrl, { signal: AbortSignal.timeout(15_000) })
69
- if (!res.ok) {
70
- return NextResponse.json(
71
- { error: `Download failed (HTTP ${res.status}) from ${rawUrl}` },
72
- { status: 502 },
73
- )
74
- }
75
- const contentType = res.headers.get('content-type') || ''
76
- let code = await res.text()
77
-
78
- // Reject HTML responses (likely a GitHub page, not raw content)
79
- if (contentType.includes('text/html') && code.includes('<!DOCTYPE')) {
80
- return NextResponse.json(
81
- { error: 'URL returned an HTML page instead of JavaScript. Use a raw/direct link to the .js file.' },
82
- { status: 400 },
83
- )
84
- }
85
-
86
- // Compatibility fix: Strip node-fetch requires if present, as modern Node has global fetch
87
- code = code.replace(/const\s+fetch\s*=\s*require\(['"]node-fetch['"]\);?/g, '// node-fetch stripped for compatibility')
88
- code = code.replace(/import\s+fetch\s+from\s+['"]node-fetch['"];?/g, '// node-fetch stripped for compatibility')
89
-
90
- // Ensure plugins directory exists
91
- if (!fs.existsSync(PLUGINS_DIR)) {
92
- fs.mkdirSync(PLUGINS_DIR, { recursive: true })
93
- }
94
-
95
- const dest = path.join(PLUGINS_DIR, sanitized)
96
- fs.writeFileSync(dest, code, 'utf8')
97
-
98
- // Force plugin manager to re-scan so the new plugin appears in listings
99
- getPluginManager().reload()
100
-
101
- return NextResponse.json({ ok: true, filename: sanitized })
17
+ const sanitizedFilename = sanitizePluginFilename(filename)
18
+ const installed = await getPluginManager().installPluginFromUrl(url, sanitizedFilename)
19
+ return NextResponse.json({ ok: true, filename: installed.filename, hash: installed.sourceHash })
102
20
  } catch (err: unknown) {
103
21
  const msg = err instanceof Error ? err.message : String(err)
104
- const isTimeout = msg.includes('abort') || msg.includes('timeout')
22
+ const isTimeout = /abort|timeout/i.test(msg)
23
+ const status = /valid HTTPS URL|Filename|Invalid filename|HTML page|too large/i.test(msg)
24
+ ? 400
25
+ : isTimeout
26
+ ? 504
27
+ : 500
105
28
  return NextResponse.json(
106
- { error: isTimeout ? 'Download timed out — the plugin URL may be unreachable' : `Install failed: ${msg}` },
107
- { status: isTimeout ? 504 : 500 },
29
+ { error: isTimeout ? 'Download timed out — the plugin URL may be unreachable' : msg },
30
+ { status },
108
31
  )
109
32
  }
110
33
  }
@@ -1,31 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { getPluginManager } from '@/lib/server/plugins'
3
-
4
- // Ensure all builtin plugins are registered by importing their modules
5
- import '@/lib/server/session-tools/shell'
6
- import '@/lib/server/session-tools/file'
7
- import '@/lib/server/session-tools/edit_file'
8
- import '@/lib/server/session-tools/web'
9
- import '@/lib/server/session-tools/memory'
10
- import '@/lib/server/session-tools/platform'
11
- import '@/lib/server/session-tools/monitor'
12
- import '@/lib/server/session-tools/discovery'
13
- import '@/lib/server/session-tools/sample-ui'
14
- import '@/lib/server/session-tools/git'
15
- import '@/lib/server/session-tools/wallet'
16
- import '@/lib/server/session-tools/connector'
17
- import '@/lib/server/session-tools/http'
18
- import '@/lib/server/session-tools/sandbox'
19
- import '@/lib/server/session-tools/canvas'
20
- import '@/lib/server/session-tools/chatroom'
21
- import '@/lib/server/session-tools/delegate'
22
- import '@/lib/server/session-tools/schedule'
23
- import '@/lib/server/session-tools/session-info'
24
- import '@/lib/server/session-tools/openclaw-nodes'
25
- import '@/lib/server/session-tools/openclaw-workspace'
26
- import '@/lib/server/session-tools/context-mgmt'
27
- import '@/lib/server/session-tools/subagent'
28
- import '@/lib/server/session-tools/plugin-creator'
3
+ import { notify } from '@/lib/server/ws-hub'
4
+ import '@/lib/server/builtin-plugins'
29
5
 
30
6
  export const dynamic = 'force-dynamic'
31
7
 
@@ -44,6 +20,7 @@ export async function POST(req: Request) {
44
20
 
45
21
  const manager = getPluginManager()
46
22
  manager.setEnabled(filename, enabled)
23
+ notify('plugins')
47
24
 
48
25
  return NextResponse.json({ ok: true })
49
26
  }
@@ -59,6 +36,7 @@ export async function DELETE(req: Request) {
59
36
  if (!deleted) {
60
37
  return NextResponse.json({ error: 'Cannot delete built-in or non-existent plugin' }, { status: 400 })
61
38
  }
39
+ notify('plugins')
62
40
  return NextResponse.json({ ok: true })
63
41
  }
64
42
 
@@ -71,11 +49,13 @@ export async function PATCH(req: Request) {
71
49
 
72
50
  if (all) {
73
51
  await manager.updateAllPlugins()
52
+ notify('plugins')
74
53
  return NextResponse.json({ ok: true, message: 'All plugins updated' })
75
54
  }
76
55
 
77
56
  if (id) {
78
57
  await manager.updatePlugin(id)
58
+ notify('plugins')
79
59
  return NextResponse.json({ ok: true, message: `Plugin ${id} updated` })
80
60
  }
81
61
 
@@ -0,0 +1,40 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getPluginManager } from '@/lib/server/plugins'
3
+ import '@/lib/server/builtin-plugins'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ /** GET /api/plugins/settings?pluginId=X — read per-plugin settings */
8
+ export async function GET(req: Request) {
9
+ const { searchParams } = new URL(req.url)
10
+ const pluginId = searchParams.get('pluginId')
11
+ if (!pluginId) {
12
+ return NextResponse.json({ error: 'pluginId required' }, { status: 400 })
13
+ }
14
+
15
+ return NextResponse.json(getPluginManager().getPublicPluginSettings(pluginId))
16
+ }
17
+
18
+ /** PUT /api/plugins/settings?pluginId=X — write per-plugin settings */
19
+ export async function PUT(req: Request) {
20
+ const { searchParams } = new URL(req.url)
21
+ const pluginId = searchParams.get('pluginId')
22
+ if (!pluginId) {
23
+ return NextResponse.json({ error: 'pluginId required' }, { status: 400 })
24
+ }
25
+
26
+ try {
27
+ const body = await req.json() as Record<string, unknown>
28
+ const saved = getPluginManager().setPluginSettings(pluginId, body)
29
+ return NextResponse.json({
30
+ ok: true,
31
+ values: saved,
32
+ configuredSecretFields: getPluginManager().getPublicPluginSettings(pluginId).configuredSecretFields,
33
+ })
34
+ } catch (err: unknown) {
35
+ return NextResponse.json(
36
+ { error: err instanceof Error ? err.message : String(err) },
37
+ { status: 400 },
38
+ )
39
+ }
40
+ }
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { getPluginManager } from '@/lib/server/plugins'
3
+ import '@/lib/server/builtin-plugins'
3
4
 
4
5
  export const dynamic = 'force-dynamic'
5
6
 
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadSettings, saveSettings } from '@/lib/server/storage'
2
+ import { loadPublicSettings, loadSettings, saveSettings } from '@/lib/server/storage'
3
3
  import { DEFAULT_DELEGATION_MAX_DEPTH } from '@/lib/runtime-loop'
4
4
  export const dynamic = 'force-dynamic'
5
5
 
@@ -20,6 +20,9 @@ const TASK_QG_MIN_RESULT_MIN = 10
20
20
  const TASK_QG_MIN_RESULT_MAX = 2000
21
21
  const TASK_QG_MIN_EVIDENCE_MIN = 0
22
22
  const TASK_QG_MIN_EVIDENCE_MAX = 8
23
+ const SESSION_RESET_TIMEOUT_MIN = 0
24
+ const SESSION_RESET_TIMEOUT_MAX = 365 * 24 * 60 * 60
25
+ const SECRET_SETTING_KEYS = ['elevenLabsApiKey', 'tavilyApiKey', 'braveApiKey'] as const
23
26
 
24
27
  function parseIntSetting(value: unknown, fallback: number, min: number, max: number): number {
25
28
  const parsed = typeof value === 'number'
@@ -42,13 +45,25 @@ function parseBoolSetting(value: unknown, fallback: boolean): boolean {
42
45
  }
43
46
 
44
47
  export async function GET(_req: Request) {
45
- return NextResponse.json(loadSettings())
48
+ return NextResponse.json(loadPublicSettings())
46
49
  }
47
50
 
48
51
  export async function PUT(req: Request) {
49
- const body = await req.json()
52
+ const body = await req.json() as Record<string, unknown>
53
+ const sanitizedBody: Record<string, unknown> = { ...body }
54
+
55
+ delete sanitizedBody.__encryptedAppSettings
56
+
57
+ for (const key of SECRET_SETTING_KEYS) {
58
+ const configuredKey = `${key}Configured`
59
+ if (sanitizedBody[key] === null && sanitizedBody[configuredKey] === true) {
60
+ delete sanitizedBody[key]
61
+ }
62
+ delete sanitizedBody[configuredKey]
63
+ }
64
+
50
65
  const settings = loadSettings()
51
- Object.assign(settings, body)
66
+ Object.assign(settings, sanitizedBody)
52
67
 
53
68
  const nextDepth = parseIntSetting(
54
69
  settings.memoryReferenceDepth ?? settings.memoryMaxDepth,
@@ -116,16 +131,43 @@ export async function PUT(req: Request) {
116
131
  settings.taskQualityGateRequireArtifact = parseBoolSetting(settings.taskQualityGateRequireArtifact, false)
117
132
  settings.taskQualityGateRequireReport = parseBoolSetting(settings.taskQualityGateRequireReport, false)
118
133
  settings.integrityMonitorEnabled = parseBoolSetting(settings.integrityMonitorEnabled, true)
134
+ settings.sessionResetMode = settings.sessionResetMode === 'daily' ? 'daily' : settings.sessionResetMode === 'idle' ? 'idle' : null
135
+ settings.sessionIdleTimeoutSec = parseIntSetting(
136
+ settings.sessionIdleTimeoutSec,
137
+ 12 * 60 * 60,
138
+ SESSION_RESET_TIMEOUT_MIN,
139
+ SESSION_RESET_TIMEOUT_MAX,
140
+ )
141
+ settings.sessionMaxAgeSec = parseIntSetting(
142
+ settings.sessionMaxAgeSec,
143
+ 7 * 24 * 60 * 60,
144
+ SESSION_RESET_TIMEOUT_MIN,
145
+ SESSION_RESET_TIMEOUT_MAX,
146
+ )
147
+ if (typeof settings.sessionDailyResetAt === 'string') settings.sessionDailyResetAt = settings.sessionDailyResetAt.trim() || null
148
+ if (typeof settings.sessionResetTimezone === 'string') settings.sessionResetTimezone = settings.sessionResetTimezone.trim() || null
119
149
 
120
150
  saveSettings(settings)
121
151
 
122
152
  // Restart heartbeat service when heartbeat-related settings change
123
- const heartbeatKeys = ['heartbeatIntervalSec', 'heartbeatInterval', 'heartbeatPrompt', 'heartbeatEnabled', 'heartbeatActiveStart', 'heartbeatActiveEnd']
124
- if (heartbeatKeys.some((k) => k in body)) {
153
+ const heartbeatKeys = [
154
+ 'heartbeatIntervalSec',
155
+ 'heartbeatInterval',
156
+ 'heartbeatPrompt',
157
+ 'heartbeatEnabled',
158
+ 'heartbeatActiveStart',
159
+ 'heartbeatActiveEnd',
160
+ 'sessionResetMode',
161
+ 'sessionIdleTimeoutSec',
162
+ 'sessionMaxAgeSec',
163
+ 'sessionDailyResetAt',
164
+ 'sessionResetTimezone',
165
+ ]
166
+ if (heartbeatKeys.some((k) => k in sanitizedBody)) {
125
167
  import('@/lib/server/heartbeat-service').then(({ restartHeartbeatService }) => {
126
168
  restartHeartbeatService()
127
169
  }).catch(() => { /* heartbeat service may not be initialized yet */ })
128
170
  }
129
171
 
130
- return NextResponse.json(settings)
172
+ return NextResponse.json(loadPublicSettings())
131
173
  }
@@ -1,6 +1,6 @@
1
1
  import { genId } from '@/lib/id'
2
2
  import { NextResponse } from 'next/server'
3
- import { loadTasks, saveTasks, logActivity, loadSettings } from '@/lib/server/storage'
3
+ import { loadAgents, loadSettings, loadTasks, logActivity, upsertStoredItems, upsertTask } from '@/lib/server/storage'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
5
  import { disableSessionHeartbeat, enqueueTask, recoverStalledRunningTasks, validateCompletedTasksQueue } from '@/lib/server/queue'
6
6
  import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
@@ -13,6 +13,7 @@ import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
13
13
  import { validateDag, cascadeUnblock } from '@/lib/server/dag-validation'
14
14
  import { getPluginManager } from '@/lib/server/plugins'
15
15
  import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
16
+ import '@/lib/server/builtin-plugins'
16
17
 
17
18
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
18
19
  // Keep completed queue integrity even if daemon is not running.
@@ -90,7 +91,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
90
91
  }
91
92
  }
92
93
 
93
- saveTasks(tasks)
94
+ upsertTask(id, tasks[id])
94
95
  logActivity({ entityType: 'task', entityId: id, action: 'updated', actor: 'user', summary: `Task updated: "${tasks[id].title}" (${prevStatus} → ${tasks[id].status})` })
95
96
  if (prevStatus !== tasks[id].status) {
96
97
  pushMainLoopEventToMainSessions({
@@ -111,7 +112,12 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
111
112
  })
112
113
 
113
114
  if (tasks[id].status === 'completed') {
114
- getPluginManager().runHook('onTaskComplete', { taskId: id, result: tasks[id].result })
115
+ const agentPlugins = tasks[id].agentId ? (loadAgents()[tasks[id].agentId]?.plugins || []) : []
116
+ getPluginManager().runHook(
117
+ 'onTaskComplete',
118
+ { taskId: id, result: tasks[id].result },
119
+ { enabledIds: agentPlugins },
120
+ )
115
121
  }
116
122
 
117
123
  // Enqueue system event + heartbeat wake
@@ -131,7 +137,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
131
137
  // Revert status change and reject
132
138
  tasks[id].status = prevStatus
133
139
  tasks[id].updatedAt = Date.now()
134
- saveTasks(tasks)
140
+ upsertTask(id, tasks[id])
135
141
  return NextResponse.json(
136
142
  { error: 'Cannot queue: blocked by incomplete tasks', blockedBy: incompleteBlocker },
137
143
  { status: 409 },
@@ -143,7 +149,10 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
143
149
  if (tasks[id].status === 'completed') {
144
150
  const unblockedIds = cascadeUnblock(tasks, id)
145
151
  if (unblockedIds.length > 0) {
146
- saveTasks(tasks)
152
+ upsertStoredItems('tasks', [
153
+ [id, tasks[id]],
154
+ ...unblockedIds.map((uid) => [uid, tasks[uid]] as [string, any]),
155
+ ])
147
156
  for (const uid of unblockedIds) {
148
157
  enqueueTask(uid)
149
158
  }
@@ -168,7 +177,7 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
168
177
  tasks[id].status = 'archived'
169
178
  tasks[id].archivedAt = Date.now()
170
179
  tasks[id].updatedAt = Date.now()
171
- saveTasks(tasks)
180
+ upsertTask(id, tasks[id])
172
181
  logActivity({ entityType: 'task', entityId: id, action: 'deleted', actor: 'user', summary: `Task archived: "${tasks[id].title}"` })
173
182
  pushMainLoopEventToMainSessions({
174
183
  type: 'task_archived',
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadTasks, saveTasks, logActivity } from '@/lib/server/storage'
2
+ import { loadTasks, logActivity, upsertStoredItems } from '@/lib/server/storage'
3
3
  import { enqueueTask, disableSessionHeartbeat } from '@/lib/server/queue'
4
4
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
5
5
  import { notify } from '@/lib/server/ws-hub'
@@ -82,7 +82,7 @@ export async function POST(req: Request) {
82
82
  }
83
83
  }
84
84
 
85
- saveTasks(tasks)
85
+ upsertStoredItems('tasks', results.map((id) => [id, tasks[id]] as [string, any]))
86
86
 
87
87
  if (updated > 0) {
88
88
  const action = body.status