@swarmclawai/swarmclaw 0.4.0 → 0.4.5

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 (144) hide show
  1. package/README.md +13 -2
  2. package/next.config.ts +8 -0
  3. package/package.json +2 -1
  4. package/src/app/api/agents/[id]/route.ts +20 -21
  5. package/src/app/api/agents/[id]/thread/route.ts +2 -2
  6. package/src/app/api/agents/route.ts +3 -2
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/connectors/[id]/route.ts +10 -3
  9. package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
  10. package/src/app/api/connectors/route.ts +6 -3
  11. package/src/app/api/credentials/[id]/route.ts +2 -1
  12. package/src/app/api/credentials/route.ts +2 -2
  13. package/src/app/api/documents/route.ts +2 -2
  14. package/src/app/api/files/serve/route.ts +8 -0
  15. package/src/app/api/knowledge/[id]/route.ts +5 -4
  16. package/src/app/api/knowledge/upload/route.ts +2 -2
  17. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  18. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  19. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  20. package/src/app/api/mcp-servers/route.ts +2 -2
  21. package/src/app/api/memory/[id]/route.ts +9 -8
  22. package/src/app/api/memory/route.ts +2 -2
  23. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  24. package/src/app/api/openclaw/directory/route.ts +26 -0
  25. package/src/app/api/openclaw/discover/route.ts +61 -0
  26. package/src/app/api/openclaw/sync/route.ts +30 -0
  27. package/src/app/api/orchestrator/run/route.ts +2 -2
  28. package/src/app/api/projects/[id]/route.ts +55 -0
  29. package/src/app/api/projects/route.ts +27 -0
  30. package/src/app/api/providers/[id]/models/route.ts +2 -1
  31. package/src/app/api/providers/[id]/route.ts +13 -15
  32. package/src/app/api/providers/route.ts +2 -2
  33. package/src/app/api/schedules/[id]/route.ts +16 -18
  34. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  35. package/src/app/api/schedules/route.ts +2 -2
  36. package/src/app/api/secrets/[id]/route.ts +16 -17
  37. package/src/app/api/secrets/route.ts +2 -2
  38. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  39. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  40. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  41. package/src/app/api/sessions/[id]/messages/route.ts +2 -1
  42. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  43. package/src/app/api/sessions/[id]/route.ts +2 -1
  44. package/src/app/api/sessions/route.ts +2 -2
  45. package/src/app/api/skills/[id]/route.ts +23 -21
  46. package/src/app/api/skills/import/route.ts +2 -2
  47. package/src/app/api/skills/route.ts +2 -2
  48. package/src/app/api/tasks/[id]/approve/route.ts +2 -1
  49. package/src/app/api/tasks/[id]/route.ts +6 -5
  50. package/src/app/api/tasks/route.ts +2 -2
  51. package/src/app/api/tts/stream/route.ts +48 -0
  52. package/src/app/api/upload/route.ts +2 -2
  53. package/src/app/api/uploads/[filename]/route.ts +4 -1
  54. package/src/app/api/webhooks/[id]/route.ts +29 -31
  55. package/src/app/api/webhooks/route.ts +2 -2
  56. package/src/app/page.tsx +3 -24
  57. package/src/cli/index.js +28 -0
  58. package/src/cli/index.ts +1 -1
  59. package/src/cli/spec.js +2 -0
  60. package/src/components/agents/agent-list.tsx +3 -1
  61. package/src/components/agents/agent-sheet.tsx +116 -14
  62. package/src/components/chat/chat-area.tsx +27 -4
  63. package/src/components/chat/chat-header.tsx +141 -29
  64. package/src/components/chat/tool-call-bubble.tsx +9 -3
  65. package/src/components/chat/voice-overlay.tsx +80 -0
  66. package/src/components/connectors/connector-list.tsx +6 -2
  67. package/src/components/connectors/connector-sheet.tsx +31 -7
  68. package/src/components/layout/app-layout.tsx +47 -25
  69. package/src/components/projects/project-list.tsx +122 -0
  70. package/src/components/projects/project-sheet.tsx +135 -0
  71. package/src/components/schedules/schedule-list.tsx +3 -1
  72. package/src/components/sessions/new-session-sheet.tsx +6 -6
  73. package/src/components/sessions/session-card.tsx +1 -1
  74. package/src/components/sessions/session-list.tsx +7 -7
  75. package/src/components/shared/connector-platform-icon.tsx +4 -0
  76. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  77. package/src/components/shared/settings/section-orchestrator.tsx +1 -2
  78. package/src/components/shared/settings/section-web-search.tsx +56 -0
  79. package/src/components/shared/settings/settings-page.tsx +73 -0
  80. package/src/components/skills/skill-list.tsx +2 -1
  81. package/src/components/tasks/task-list.tsx +5 -2
  82. package/src/hooks/use-continuous-speech.ts +144 -0
  83. package/src/hooks/use-view-router.ts +52 -0
  84. package/src/hooks/use-voice-conversation.ts +80 -0
  85. package/src/lib/id.ts +6 -0
  86. package/src/lib/projects.ts +13 -0
  87. package/src/lib/provider-sets.ts +5 -0
  88. package/src/lib/providers/anthropic.ts +14 -1
  89. package/src/lib/providers/index.ts +6 -0
  90. package/src/lib/providers/ollama.ts +9 -1
  91. package/src/lib/providers/openai.ts +9 -1
  92. package/src/lib/providers/openclaw.ts +11 -0
  93. package/src/lib/server/api-routes.test.ts +5 -6
  94. package/src/lib/server/build-llm.ts +17 -4
  95. package/src/lib/server/chat-execution.ts +38 -4
  96. package/src/lib/server/collection-helpers.ts +54 -0
  97. package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
  98. package/src/lib/server/connectors/bluebubbles.ts +357 -0
  99. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  100. package/src/lib/server/connectors/googlechat.ts +46 -7
  101. package/src/lib/server/connectors/manager.ts +392 -3
  102. package/src/lib/server/connectors/media.ts +2 -2
  103. package/src/lib/server/connectors/openclaw.ts +64 -0
  104. package/src/lib/server/connectors/pairing.test.ts +99 -0
  105. package/src/lib/server/connectors/pairing.ts +256 -0
  106. package/src/lib/server/connectors/signal.ts +1 -0
  107. package/src/lib/server/connectors/teams.ts +5 -5
  108. package/src/lib/server/connectors/types.ts +10 -0
  109. package/src/lib/server/execution-log.ts +3 -3
  110. package/src/lib/server/heartbeat-service.ts +1 -1
  111. package/src/lib/server/knowledge-db.test.ts +2 -33
  112. package/src/lib/server/main-agent-loop.ts +6 -6
  113. package/src/lib/server/memory-db.ts +6 -6
  114. package/src/lib/server/openclaw-approvals.ts +105 -0
  115. package/src/lib/server/openclaw-sync.ts +496 -0
  116. package/src/lib/server/orchestrator-lg.ts +30 -9
  117. package/src/lib/server/orchestrator.ts +4 -4
  118. package/src/lib/server/process-manager.ts +2 -2
  119. package/src/lib/server/queue.ts +22 -10
  120. package/src/lib/server/scheduler.ts +2 -2
  121. package/src/lib/server/session-mailbox.ts +2 -2
  122. package/src/lib/server/session-run-manager.ts +2 -2
  123. package/src/lib/server/session-tools/connector.ts +51 -4
  124. package/src/lib/server/session-tools/crud.ts +3 -3
  125. package/src/lib/server/session-tools/delegate.ts +3 -3
  126. package/src/lib/server/session-tools/file.ts +176 -3
  127. package/src/lib/server/session-tools/index.ts +2 -0
  128. package/src/lib/server/session-tools/memory.ts +2 -2
  129. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  130. package/src/lib/server/session-tools/sandbox.ts +33 -0
  131. package/src/lib/server/session-tools/search-providers.ts +270 -0
  132. package/src/lib/server/session-tools/session-info.ts +2 -2
  133. package/src/lib/server/session-tools/web.ts +47 -66
  134. package/src/lib/server/storage.ts +12 -0
  135. package/src/lib/server/stream-agent-chat.ts +29 -0
  136. package/src/lib/server/task-result.test.ts +44 -0
  137. package/src/lib/server/task-result.ts +14 -0
  138. package/src/lib/tool-definitions.ts +5 -3
  139. package/src/lib/tts-stream.ts +130 -0
  140. package/src/lib/view-routes.ts +28 -0
  141. package/src/proxy.ts +3 -0
  142. package/src/stores/use-app-store.ts +28 -1
  143. package/src/stores/use-chat-store.ts +9 -1
  144. package/src/types/index.ts +27 -2
@@ -1,40 +1,42 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadSkills, saveSkills, deleteSkill } from '@/lib/server/storage'
3
3
  import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
4
+ import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ const ops: CollectionOps<any> = { load: loadSkills, save: saveSkills, deleteFn: deleteSkill }
4
8
 
5
9
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
10
  const { id } = await params
7
11
  const skills = loadSkills()
8
- if (!skills[id]) return new NextResponse(null, { status: 404 })
12
+ if (!skills[id]) return notFound()
9
13
  return NextResponse.json(skills[id])
10
14
  }
11
15
 
12
16
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
13
17
  const { id } = await params
14
18
  const body = await req.json()
15
- const skills = loadSkills()
16
- if (!skills[id]) return new NextResponse(null, { status: 404 })
17
- const normalized = normalizeSkillPayload({ ...skills[id], ...body })
18
- skills[id] = {
19
- ...skills[id],
20
- ...body,
21
- name: normalized.name,
22
- filename: normalized.filename,
23
- description: normalized.description,
24
- content: normalized.content,
25
- sourceUrl: normalized.sourceUrl,
26
- sourceFormat: normalized.sourceFormat,
27
- id,
28
- updatedAt: Date.now(),
29
- }
30
- saveSkills(skills)
31
- return NextResponse.json(skills[id])
19
+ const result = mutateItem(ops, id, (skill) => {
20
+ const normalized = normalizeSkillPayload({ ...skill, ...body })
21
+ return {
22
+ ...skill,
23
+ ...body,
24
+ name: normalized.name,
25
+ filename: normalized.filename,
26
+ description: normalized.description,
27
+ content: normalized.content,
28
+ sourceUrl: normalized.sourceUrl,
29
+ sourceFormat: normalized.sourceFormat,
30
+ id,
31
+ updatedAt: Date.now(),
32
+ }
33
+ })
34
+ if (!result) return notFound()
35
+ return NextResponse.json(result)
32
36
  }
33
37
 
34
38
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
35
39
  const { id } = await params
36
- const skills = loadSkills()
37
- if (!skills[id]) return new NextResponse(null, { status: 404 })
38
- deleteSkill(id)
40
+ if (!deleteItem(ops, id)) return notFound()
39
41
  return NextResponse.json({ deleted: id })
40
42
  }
@@ -1,4 +1,4 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import { NextResponse } from 'next/server'
3
3
  import { loadSkills, saveSkills } from '@/lib/server/storage'
4
4
  import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
@@ -47,7 +47,7 @@ export async function POST(req: Request) {
47
47
  })
48
48
 
49
49
  const skills = loadSkills()
50
- const id = crypto.randomBytes(4).toString('hex')
50
+ const id = genId()
51
51
  skills[id] = {
52
52
  id,
53
53
  name: normalized.name,
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import crypto from 'crypto'
2
+ import { genId } from '@/lib/id'
3
3
  import { loadSkills, saveSkills } from '@/lib/server/storage'
4
4
  import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
5
5
  export const dynamic = 'force-dynamic'
@@ -12,7 +12,7 @@ export async function GET(_req: Request) {
12
12
  export async function POST(req: Request) {
13
13
  const body = await req.json()
14
14
  const skills = loadSkills()
15
- const id = crypto.randomBytes(4).toString('hex')
15
+ const id = genId()
16
16
  const normalized = normalizeSkillPayload(body)
17
17
  skills[id] = {
18
18
  id,
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadTasks, saveTasks, loadAgents } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
3
4
  import { notify } from '@/lib/server/ws-hub'
4
5
  import { getCheckpointSaver } from '@/lib/server/langgraph-checkpoint'
5
6
 
@@ -10,7 +11,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
10
11
 
11
12
  const tasks = loadTasks()
12
13
  const task = tasks[id]
13
- if (!task) return new NextResponse(null, { status: 404 })
14
+ if (!task) return notFound()
14
15
  if (!task.pendingApproval) {
15
16
  return NextResponse.json({ error: 'No pending approval on this task' }, { status: 400 })
16
17
  }
@@ -1,6 +1,7 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import { NextResponse } from 'next/server'
3
3
  import { loadTasks, saveTasks } from '@/lib/server/storage'
4
+ import { notFound } from '@/lib/server/collection-helpers'
4
5
  import { disableSessionHeartbeat, enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
5
6
  import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
6
7
  import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
@@ -13,7 +14,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
13
14
 
14
15
  const { id } = await params
15
16
  const tasks = loadTasks()
16
- if (!tasks[id]) return new NextResponse(null, { status: 404 })
17
+ if (!tasks[id]) return notFound()
17
18
  return NextResponse.json(tasks[id])
18
19
  }
19
20
 
@@ -21,7 +22,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
21
22
  const { id } = await params
22
23
  const body = await req.json()
23
24
  const tasks = loadTasks()
24
- if (!tasks[id]) return new NextResponse(null, { status: 404 })
25
+ if (!tasks[id]) return notFound()
25
26
 
26
27
  const prevStatus = tasks[id].status
27
28
 
@@ -55,7 +56,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
55
56
  tasks[id].error = formatValidationFailure(validation.reasons).slice(0, 500)
56
57
  if (!tasks[id].comments) tasks[id].comments = []
57
58
  tasks[id].comments.push({
58
- id: crypto.randomBytes(4).toString('hex'),
59
+ id: genId(),
59
60
  author: 'System',
60
61
  text: `Completion validation failed.\n\n${validation.reasons.map((r) => `- ${r}`).join('\n')}`,
61
62
  createdAt: Date.now(),
@@ -88,7 +89,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
88
89
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
89
90
  const { id } = await params
90
91
  const tasks = loadTasks()
91
- if (!tasks[id]) return new NextResponse(null, { status: 404 })
92
+ if (!tasks[id]) return notFound()
92
93
 
93
94
  // Soft delete: move to archived status instead of hard delete
94
95
  tasks[id].status = 'archived'
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import crypto from 'crypto'
2
+ import { genId } from '@/lib/id'
3
3
  import { loadTasks, saveTasks, loadSettings } from '@/lib/server/storage'
4
4
  import { enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
5
5
  import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
@@ -54,7 +54,7 @@ export async function DELETE(req: Request) {
54
54
 
55
55
  export async function POST(req: Request) {
56
56
  const body = await req.json()
57
- const id = crypto.randomBytes(4).toString('hex')
57
+ const id = genId()
58
58
  const now = Date.now()
59
59
  const tasks = loadTasks()
60
60
  const settings = loadSettings()
@@ -0,0 +1,48 @@
1
+ import { loadSettings } from '@/lib/server/storage'
2
+
3
+ export async function POST(req: Request) {
4
+ const settings = loadSettings()
5
+ const ELEVENLABS_KEY = settings.elevenLabsApiKey || process.env.ELEVENLABS_API_KEY
6
+ const ELEVENLABS_VOICE = settings.elevenLabsVoiceId || process.env.ELEVENLABS_VOICE || 'JBFqnCBsd6RMkjVDRZzb'
7
+
8
+ if (!ELEVENLABS_KEY) {
9
+ return new Response('No ElevenLabs API key. Set one in Settings > Voice.', { status: 500 })
10
+ }
11
+
12
+ const { text } = await req.json()
13
+ if (!text?.trim()) {
14
+ return new Response('No text provided', { status: 400 })
15
+ }
16
+
17
+ const apiRes = await fetch(
18
+ `https://api.elevenlabs.io/v1/text-to-speech/${ELEVENLABS_VOICE}/stream`,
19
+ {
20
+ method: 'POST',
21
+ headers: {
22
+ 'xi-api-key': ELEVENLABS_KEY,
23
+ 'Content-Type': 'application/json',
24
+ 'Accept': 'audio/mpeg',
25
+ },
26
+ body: JSON.stringify({
27
+ text: text.slice(0, 2000),
28
+ model_id: 'eleven_multilingual_v2',
29
+ voice_settings: { stability: 0.5, similarity_boost: 0.75 },
30
+ output_format: 'mp3_22050_32',
31
+ }),
32
+ },
33
+ )
34
+
35
+ if (!apiRes.ok) {
36
+ const err = await apiRes.text()
37
+ return new Response(err, { status: apiRes.status })
38
+ }
39
+
40
+ // Pipe the streaming response directly
41
+ return new Response(apiRes.body, {
42
+ headers: {
43
+ 'Content-Type': 'audio/mpeg',
44
+ 'Transfer-Encoding': 'chunked',
45
+ 'Cache-Control': 'no-cache',
46
+ },
47
+ })
48
+ }
@@ -1,13 +1,13 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import fs from 'fs'
3
3
  import path from 'path'
4
- import crypto from 'crypto'
4
+ import { genId } from '@/lib/id'
5
5
  import { UPLOAD_DIR } from '@/lib/server/storage'
6
6
 
7
7
  export async function POST(req: Request) {
8
8
  const filename = req.headers.get('x-filename') || 'image.png'
9
9
  const buf = Buffer.from(await req.arrayBuffer())
10
- const name = crypto.randomBytes(4).toString('hex') + '-' + filename.replace(/[^a-zA-Z0-9._-]/g, '_')
10
+ const name = genId() + '-' + filename.replace(/[^a-zA-Z0-9._-]/g, '_')
11
11
  const filePath = path.join(UPLOAD_DIR, name)
12
12
 
13
13
  if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
@@ -1,4 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import { notFound } from '@/lib/server/collection-helpers'
2
3
  import fs from 'fs'
3
4
  import path from 'path'
4
5
  import { UPLOAD_DIR } from '@/lib/server/storage'
@@ -43,16 +44,18 @@ export async function GET(_req: Request, { params }: { params: Promise<{ filenam
43
44
  const filePath = path.join(UPLOAD_DIR, safeName)
44
45
 
45
46
  if (!fs.existsSync(filePath)) {
46
- return new NextResponse(null, { status: 404 })
47
+ return notFound()
47
48
  }
48
49
 
49
50
  const ext = path.extname(safeName).toLowerCase()
50
51
  const contentType = MIME_TYPES[ext] || 'application/octet-stream'
51
52
  const data = fs.readFileSync(filePath)
52
53
 
54
+ const inline = contentType.startsWith('image/') || contentType.startsWith('video/') || contentType.startsWith('text/') || contentType === 'application/pdf'
53
55
  return new NextResponse(data, {
54
56
  headers: {
55
57
  'Content-Type': contentType,
58
+ 'Content-Disposition': inline ? 'inline' : `attachment; filename="${path.basename(safeName)}"`,
56
59
  'Cache-Control': 'public, max-age=86400',
57
60
  },
58
61
  })
@@ -1,8 +1,12 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import { NextResponse } from 'next/server'
3
3
  import { loadAgents, loadSessions, loadWebhooks, saveSessions, saveWebhooks, appendWebhookLog } from '@/lib/server/storage'
4
4
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
5
5
  import { enqueueSessionRun } from '@/lib/server/session-run-manager'
6
+ import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
7
+
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ const ops: CollectionOps<any> = { load: loadWebhooks, save: saveWebhooks }
6
10
 
7
11
  function normalizeEvents(value: unknown): string[] {
8
12
  if (!Array.isArray(value)) return []
@@ -22,36 +26,30 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
22
26
  const { id } = await params
23
27
  const webhooks = loadWebhooks()
24
28
  const webhook = webhooks[id]
25
- if (!webhook) return NextResponse.json({ error: 'Webhook not found' }, { status: 404 })
29
+ if (!webhook) return notFound('Webhook not found')
26
30
  return NextResponse.json(webhook)
27
31
  }
28
32
 
29
33
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
30
34
  const { id } = await params
31
35
  const body = await req.json().catch(() => ({}))
32
- const webhooks = loadWebhooks()
33
- const webhook = webhooks[id]
34
- if (!webhook) return NextResponse.json({ error: 'Webhook not found' }, { status: 404 })
35
-
36
- if (body.name !== undefined) webhook.name = body.name
37
- if (body.source !== undefined) webhook.source = body.source
38
- if (body.events !== undefined) webhook.events = normalizeEvents(body.events)
39
- if (body.agentId !== undefined) webhook.agentId = body.agentId
40
- if (body.secret !== undefined) webhook.secret = body.secret
41
- if (body.isEnabled !== undefined) webhook.isEnabled = !!body.isEnabled
42
- webhook.updatedAt = Date.now()
43
-
44
- webhooks[id] = webhook
45
- saveWebhooks(webhooks)
46
- return NextResponse.json(webhook)
36
+ const result = mutateItem(ops, id, (webhook) => {
37
+ if (body.name !== undefined) webhook.name = body.name
38
+ if (body.source !== undefined) webhook.source = body.source
39
+ if (body.events !== undefined) webhook.events = normalizeEvents(body.events)
40
+ if (body.agentId !== undefined) webhook.agentId = body.agentId
41
+ if (body.secret !== undefined) webhook.secret = body.secret
42
+ if (body.isEnabled !== undefined) webhook.isEnabled = !!body.isEnabled
43
+ webhook.updatedAt = Date.now()
44
+ return webhook
45
+ })
46
+ if (!result) return notFound('Webhook not found')
47
+ return NextResponse.json(result)
47
48
  }
48
49
 
49
50
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
50
51
  const { id } = await params
51
- const webhooks = loadWebhooks()
52
- if (!webhooks[id]) return NextResponse.json({ error: 'Webhook not found' }, { status: 404 })
53
- delete webhooks[id]
54
- saveWebhooks(webhooks)
52
+ if (!deleteItem(ops, id)) return notFound('Webhook not found')
55
53
  return NextResponse.json({ ok: true })
56
54
  }
57
55
 
@@ -59,10 +57,10 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
59
57
  const { id } = await params
60
58
  const webhooks = loadWebhooks()
61
59
  const webhook = webhooks[id]
62
- if (!webhook) return NextResponse.json({ error: 'Webhook not found' }, { status: 404 })
60
+ if (!webhook) return notFound('Webhook not found')
63
61
  if (webhook.isEnabled === false) {
64
- appendWebhookLog(crypto.randomBytes(8).toString('hex'), {
65
- id: crypto.randomBytes(8).toString('hex'), webhookId: id, event: 'unknown',
62
+ appendWebhookLog(genId(8), {
63
+ id: genId(8), webhookId: id, event: 'unknown',
66
64
  payload: '', status: 'error', error: 'Webhook is disabled', timestamp: Date.now(),
67
65
  })
68
66
  return NextResponse.json({ error: 'Webhook is disabled' }, { status: 409 })
@@ -73,8 +71,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
73
71
  const url = new URL(req.url)
74
72
  const provided = req.headers.get('x-webhook-secret') || url.searchParams.get('secret') || ''
75
73
  if (provided !== secret) {
76
- appendWebhookLog(crypto.randomBytes(8).toString('hex'), {
77
- id: crypto.randomBytes(8).toString('hex'), webhookId: id, event: 'unknown',
74
+ appendWebhookLog(genId(8), {
75
+ id: genId(8), webhookId: id, event: 'unknown',
78
76
  payload: '', status: 'error', error: 'Invalid webhook secret', timestamp: Date.now(),
79
77
  })
80
78
  return NextResponse.json({ error: 'Invalid webhook secret' }, { status: 401 })
@@ -122,8 +120,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
122
120
  const agents = loadAgents()
123
121
  const agent = webhook.agentId ? agents[webhook.agentId] : null
124
122
  if (!agent) {
125
- appendWebhookLog(crypto.randomBytes(8).toString('hex'), {
126
- id: crypto.randomBytes(8).toString('hex'), webhookId: id, event: incomingEvent,
123
+ appendWebhookLog(genId(8), {
124
+ id: genId(8), webhookId: id, event: incomingEvent,
127
125
  payload: (rawBody || '').slice(0, 2000), status: 'error', error: 'Webhook agent is not configured or missing', timestamp: Date.now(),
128
126
  })
129
127
  return NextResponse.json({ error: 'Webhook agent is not configured or missing' }, { status: 400 })
@@ -133,7 +131,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
133
131
  const sessionName = `webhook:${id}`
134
132
  let session = Object.values(sessions).find((s: any) => s.name === sessionName && s.agentId === agent.id) as any
135
133
  if (!session) {
136
- const sessionId = crypto.randomBytes(4).toString('hex')
134
+ const sessionId = genId()
137
135
  const now = Date.now()
138
136
  session = {
139
137
  id: sessionId,
@@ -189,8 +187,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
189
187
  mode: 'followup',
190
188
  })
191
189
 
192
- appendWebhookLog(crypto.randomBytes(8).toString('hex'), {
193
- id: crypto.randomBytes(8).toString('hex'), webhookId: id, event: incomingEvent,
190
+ appendWebhookLog(genId(8), {
191
+ id: genId(8), webhookId: id, event: incomingEvent,
194
192
  payload: (rawBody || '').slice(0, 2000), status: 'success',
195
193
  sessionId: session.id, runId: run.runId, timestamp: Date.now(),
196
194
  })
@@ -1,4 +1,4 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import { NextResponse } from 'next/server'
3
3
  import { loadWebhooks, saveWebhooks } from '@/lib/server/storage'
4
4
  export const dynamic = 'force-dynamic'
@@ -19,7 +19,7 @@ export async function GET(_req: Request) {
19
19
  export async function POST(req: Request) {
20
20
  const body = await req.json().catch(() => ({}))
21
21
  const webhooks = loadWebhooks()
22
- const id = crypto.randomBytes(4).toString('hex')
22
+ const id = genId()
23
23
  const now = Date.now()
24
24
 
25
25
  webhooks[id] = {
package/src/app/page.tsx CHANGED
@@ -10,6 +10,7 @@ import { AccessKeyGate } from '@/components/auth/access-key-gate'
10
10
  import { UserPicker } from '@/components/auth/user-picker'
11
11
  import { SetupWizard } from '@/components/auth/setup-wizard'
12
12
  import { AppLayout } from '@/components/layout/app-layout'
13
+ import { useViewRouter } from '@/hooks/use-view-router'
13
14
 
14
15
  export default function Home() {
15
16
  const currentUser = useAppStore((s) => s.currentUser)
@@ -17,7 +18,6 @@ export default function Home() {
17
18
  const hydrated = useAppStore((s) => s._hydrated)
18
19
  const hydrate = useAppStore((s) => s.hydrate)
19
20
  const loadNetworkInfo = useAppStore((s) => s.loadNetworkInfo)
20
- const sessions = useAppStore((s) => s.sessions)
21
21
  const loadSessions = useAppStore((s) => s.loadSessions)
22
22
  const loadSettings = useAppStore((s) => s.loadSettings)
23
23
 
@@ -107,29 +107,6 @@ export default function Home() {
107
107
  return () => { cancelled = true }
108
108
  }, [authenticated, currentUser])
109
109
 
110
- // Keep __main__ session for backward compat — create if missing
111
- useEffect(() => {
112
- if (!authenticated || !currentUser) return
113
- const sessionList = Object.values(sessions)
114
- const mainSession = sessionList.find((s: any) => s.name === '__main__' && s.user === currentUser)
115
- if (mainSession) return
116
- let cancelled = false
117
- ;(async () => {
118
- try {
119
- const mainId = `main-${currentUser}`
120
- await api<any>('POST', '/sessions', {
121
- id: mainId,
122
- name: '__main__',
123
- user: currentUser,
124
- agentId: 'default',
125
- heartbeatEnabled: true,
126
- })
127
- if (!cancelled) await loadSessions()
128
- } catch { /* ignore */ }
129
- })()
130
- return () => { cancelled = true }
131
- }, [authenticated, currentUser, sessions, loadSessions])
132
-
133
110
  // Check if first-run setup is needed
134
111
  useEffect(() => {
135
112
  if (!authenticated || !currentUser) return
@@ -169,6 +146,8 @@ export default function Home() {
169
146
  return () => window.removeEventListener('sc_auth_required', handler)
170
147
  }, [])
171
148
 
149
+ useViewRouter()
150
+
172
151
  if (!hydrated || !authChecked) return null
173
152
  if (!authenticated) return <AccessKeyGate onAuthenticated={() => setAuthenticated(true)} />
174
153
  if (!currentUser) return <UserPicker />
package/src/cli/index.js CHANGED
@@ -55,6 +55,7 @@ const COMMAND_GROUPS = [
55
55
  cmd('create', 'POST', '/connectors', 'Create connector', { expectsJsonBody: true }),
56
56
  cmd('update', 'PUT', '/connectors/:id', 'Update connector', { expectsJsonBody: true }),
57
57
  cmd('delete', 'DELETE', '/connectors/:id', 'Delete connector'),
58
+ cmd('webhook', 'POST', '/connectors/:id/webhook', 'Trigger connector webhook ingress', { expectsJsonBody: true }),
58
59
  cmd('start', 'PUT', '/connectors/:id', 'Start connector', {
59
60
  expectsJsonBody: true,
60
61
  defaultBody: { action: 'start' },
@@ -199,6 +200,16 @@ const COMMAND_GROUPS = [
199
200
  expectsJsonBody: true,
200
201
  waitEntityFrom: 'taskId',
201
202
  }),
203
+ cmd('graph', 'GET', '/orchestrator/graph', 'Get orchestrator graph structure'),
204
+ ],
205
+ },
206
+ {
207
+ name: 'openclaw',
208
+ description: 'OpenClaw discovery and sync',
209
+ commands: [
210
+ cmd('discover', 'GET', '/openclaw/discover', 'Discover OpenClaw gateways'),
211
+ cmd('directory', 'GET', '/openclaw/directory', 'List directory entries from running OpenClaw connectors'),
212
+ cmd('sync', 'POST', '/openclaw/sync', 'Run OpenClaw sync action', { expectsJsonBody: true }),
202
213
  ],
203
214
  },
204
215
  {
@@ -208,6 +219,17 @@ const COMMAND_GROUPS = [
208
219
  cmd('manage', 'POST', '/preview-server', 'Start/stop/status/detect preview server', { expectsJsonBody: true }),
209
220
  ],
210
221
  },
222
+ {
223
+ name: 'projects',
224
+ description: 'Manage projects',
225
+ commands: [
226
+ cmd('list', 'GET', '/projects', 'List projects'),
227
+ cmd('get', 'GET', '/projects/:id', 'Get project by id'),
228
+ cmd('create', 'POST', '/projects', 'Create project', { expectsJsonBody: true }),
229
+ cmd('update', 'PUT', '/projects/:id', 'Update project', { expectsJsonBody: true }),
230
+ cmd('delete', 'DELETE', '/projects/:id', 'Delete project'),
231
+ ],
232
+ },
211
233
  {
212
234
  name: 'plugins',
213
235
  description: 'Manage plugins and marketplace',
@@ -351,6 +373,7 @@ const COMMAND_GROUPS = [
351
373
  cmd('update', 'PUT', '/tasks/:id', 'Update task', { expectsJsonBody: true }),
352
374
  cmd('delete', 'DELETE', '/tasks/:id', 'Delete task'),
353
375
  cmd('purge', 'DELETE', '/tasks', 'Bulk delete tasks', { expectsJsonBody: true }),
376
+ cmd('approve', 'POST', '/tasks/:id/approve', 'Approve or reject a pending tool execution', { expectsJsonBody: true }),
354
377
  ],
355
378
  },
356
379
  {
@@ -362,6 +385,11 @@ const COMMAND_GROUPS = [
362
385
  responseType: 'binary',
363
386
  bodyFlagMap: { text: 'text' },
364
387
  }),
388
+ cmd('stream', 'POST', '/tts/stream', 'Generate streaming TTS audio', {
389
+ expectsJsonBody: true,
390
+ responseType: 'binary',
391
+ bodyFlagMap: { text: 'text' },
392
+ }),
365
393
  ],
366
394
  },
367
395
  {
package/src/cli/index.ts CHANGED
@@ -928,7 +928,7 @@ export function buildProgram(): Command {
928
928
  connectors
929
929
  .command('create')
930
930
  .description('Create connector')
931
- .requiredOption('--platform <platform>', 'Connector platform (discord|telegram|slack|whatsapp|openclaw)')
931
+ .requiredOption('--platform <platform>', 'Connector platform (discord|telegram|slack|whatsapp|openclaw|bluebubbles|signal|teams|googlechat|matrix)')
932
932
  .requiredOption('--agent-id <agentId>', 'Agent id')
933
933
  .option('--name <name>', 'Connector name')
934
934
  .option('--credential-id <credentialId>', 'Credential id')
package/src/cli/spec.js CHANGED
@@ -127,6 +127,7 @@ const COMMAND_GROUPS = {
127
127
  run: { description: 'Run orchestrator task now', method: 'POST', path: '/orchestrator/run', waitable: true },
128
128
  runs: { description: 'List queued/running/completed runs', method: 'GET', path: '/runs' },
129
129
  'run-get': { description: 'Get run by id', method: 'GET', path: '/runs/:id', params: ['id'] },
130
+ graph: { description: 'Get orchestrator graph structure', method: 'GET', path: '/orchestrator/graph' },
130
131
  },
131
132
  },
132
133
  plugins: {
@@ -243,6 +244,7 @@ const COMMAND_GROUPS = {
243
244
  update: { description: 'Update task', method: 'PUT', path: '/tasks/:id', params: ['id'] },
244
245
  delete: { description: 'Archive task', method: 'DELETE', path: '/tasks/:id', params: ['id'] },
245
246
  archive: { description: 'Archive task', method: 'DELETE', path: '/tasks/:id', params: ['id'] },
247
+ approve: { description: 'Approve or reject a pending tool execution', method: 'POST', path: '/tasks/:id/approve', params: ['id'] },
246
248
  },
247
249
  },
248
250
  webhooks: {
@@ -16,6 +16,7 @@ export function AgentList({ inSidebar }: Props) {
16
16
  const currentUser = useAppStore((s) => s.currentUser)
17
17
  const loadSessions = useAppStore((s) => s.loadSessions)
18
18
  const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
19
+ const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
19
20
  const [search, setSearch] = useState('')
20
21
  const [filter, setFilter] = useState<'all' | 'orchestrator' | 'agent'>('all')
21
22
 
@@ -41,10 +42,11 @@ export function AgentList({ inSidebar }: Props) {
41
42
  if (search && !p.name.toLowerCase().includes(search.toLowerCase())) return false
42
43
  if (filter === 'orchestrator' && !p.isOrchestrator) return false
43
44
  if (filter === 'agent' && p.isOrchestrator) return false
45
+ if (activeProjectFilter && p.projectId !== activeProjectFilter) return false
44
46
  return true
45
47
  })
46
48
  .sort((a, b) => b.updatedAt - a.updatedAt)
47
- }, [agents, search, filter])
49
+ }, [agents, search, filter, activeProjectFilter])
48
50
 
49
51
  if (!filtered.length && !search) {
50
52
  return (