@swarmclawai/swarmclaw 0.6.8 → 0.7.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 (166) hide show
  1. package/README.md +70 -45
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +18 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  9. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  10. package/src/app/api/memory/route.ts +36 -5
  11. package/src/app/api/notifications/route.ts +3 -0
  12. package/src/app/api/plugins/install/route.ts +57 -5
  13. package/src/app/api/plugins/marketplace/route.ts +73 -22
  14. package/src/app/api/plugins/route.ts +61 -1
  15. package/src/app/api/plugins/ui/route.ts +34 -0
  16. package/src/app/api/settings/route.ts +62 -0
  17. package/src/app/api/setup/doctor/route.ts +22 -5
  18. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  19. package/src/app/api/tasks/[id]/route.ts +11 -3
  20. package/src/app/api/tasks/route.ts +8 -2
  21. package/src/app/globals.css +27 -0
  22. package/src/app/page.tsx +10 -5
  23. package/src/cli/index.js +13 -0
  24. package/src/components/activity/activity-feed.tsx +9 -2
  25. package/src/components/agents/agent-avatar.tsx +5 -1
  26. package/src/components/agents/agent-card.tsx +55 -9
  27. package/src/components/agents/agent-sheet.tsx +86 -29
  28. package/src/components/agents/inspector-panel.tsx +1 -1
  29. package/src/components/auth/access-key-gate.tsx +63 -54
  30. package/src/components/auth/user-picker.tsx +37 -32
  31. package/src/components/chat/chat-area.tsx +11 -0
  32. package/src/components/chat/chat-header.tsx +69 -25
  33. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  34. package/src/components/chat/code-block.tsx +3 -1
  35. package/src/components/chat/exec-approval-card.tsx +8 -1
  36. package/src/components/chat/message-bubble.tsx +164 -4
  37. package/src/components/chat/message-list.tsx +30 -4
  38. package/src/components/chat/session-approval-card.tsx +80 -0
  39. package/src/components/chat/streaming-bubble.tsx +6 -5
  40. package/src/components/chat/thinking-indicator.tsx +48 -12
  41. package/src/components/chat/tool-request-banner.tsx +39 -20
  42. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  43. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  44. package/src/components/connectors/connector-list.tsx +33 -11
  45. package/src/components/connectors/connector-sheet.tsx +29 -6
  46. package/src/components/home/home-view.tsx +20 -14
  47. package/src/components/input/chat-input.tsx +22 -1
  48. package/src/components/knowledge/knowledge-list.tsx +17 -18
  49. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  50. package/src/components/layout/app-layout.tsx +73 -21
  51. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  52. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  53. package/src/components/memory/memory-list.tsx +20 -13
  54. package/src/components/plugins/plugin-list.tsx +213 -59
  55. package/src/components/plugins/plugin-sheet.tsx +119 -24
  56. package/src/components/projects/project-list.tsx +17 -9
  57. package/src/components/providers/provider-list.tsx +21 -6
  58. package/src/components/providers/provider-sheet.tsx +42 -25
  59. package/src/components/runs/run-list.tsx +17 -13
  60. package/src/components/schedules/schedule-card.tsx +10 -3
  61. package/src/components/schedules/schedule-list.tsx +2 -2
  62. package/src/components/schedules/schedule-sheet.tsx +19 -7
  63. package/src/components/secrets/secret-sheet.tsx +7 -2
  64. package/src/components/secrets/secrets-list.tsx +18 -5
  65. package/src/components/sessions/new-session-sheet.tsx +183 -376
  66. package/src/components/sessions/session-card.tsx +10 -2
  67. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  68. package/src/components/shared/command-palette.tsx +13 -5
  69. package/src/components/shared/empty-state.tsx +20 -8
  70. package/src/components/shared/notification-center.tsx +134 -86
  71. package/src/components/shared/profile-sheet.tsx +4 -0
  72. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  73. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  74. package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
  75. package/src/components/skills/clawhub-browser.tsx +1 -0
  76. package/src/components/skills/skill-list.tsx +31 -12
  77. package/src/components/skills/skill-sheet.tsx +20 -7
  78. package/src/components/tasks/approvals-panel.tsx +170 -66
  79. package/src/components/tasks/task-board.tsx +20 -12
  80. package/src/components/tasks/task-card.tsx +21 -7
  81. package/src/components/tasks/task-column.tsx +4 -3
  82. package/src/components/tasks/task-list.tsx +1 -1
  83. package/src/components/tasks/task-sheet.tsx +130 -1
  84. package/src/components/ui/dialog.tsx +1 -0
  85. package/src/components/ui/sheet.tsx +1 -0
  86. package/src/components/usage/metrics-dashboard.tsx +66 -64
  87. package/src/components/wallets/wallet-panel.tsx +65 -41
  88. package/src/components/wallets/wallet-section.tsx +9 -3
  89. package/src/components/webhooks/webhook-list.tsx +21 -12
  90. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  91. package/src/lib/approval-display.test.ts +45 -0
  92. package/src/lib/approval-display.ts +62 -0
  93. package/src/lib/clipboard.ts +38 -0
  94. package/src/lib/memory.ts +8 -0
  95. package/src/lib/providers/claude-cli.ts +5 -3
  96. package/src/lib/providers/index.ts +67 -21
  97. package/src/lib/runtime-loop.ts +3 -2
  98. package/src/lib/server/approvals.ts +150 -0
  99. package/src/lib/server/chat-execution.ts +223 -62
  100. package/src/lib/server/clawhub-client.ts +82 -6
  101. package/src/lib/server/connectors/manager.ts +27 -1
  102. package/src/lib/server/cost.test.ts +73 -0
  103. package/src/lib/server/cost.ts +165 -34
  104. package/src/lib/server/daemon-state.ts +42 -0
  105. package/src/lib/server/data-dir.ts +18 -1
  106. package/src/lib/server/integrity-monitor.ts +208 -0
  107. package/src/lib/server/llm-response-cache.test.ts +102 -0
  108. package/src/lib/server/llm-response-cache.ts +227 -0
  109. package/src/lib/server/main-agent-loop.ts +1 -1
  110. package/src/lib/server/main-session.ts +6 -3
  111. package/src/lib/server/mcp-conformance.test.ts +18 -0
  112. package/src/lib/server/mcp-conformance.ts +233 -0
  113. package/src/lib/server/memory-db.ts +180 -17
  114. package/src/lib/server/memory-retrieval.test.ts +56 -0
  115. package/src/lib/server/orchestrator-lg.ts +4 -1
  116. package/src/lib/server/orchestrator.ts +4 -3
  117. package/src/lib/server/plugins.ts +650 -142
  118. package/src/lib/server/process-manager.ts +18 -0
  119. package/src/lib/server/queue.ts +253 -11
  120. package/src/lib/server/runtime-settings.ts +9 -0
  121. package/src/lib/server/session-run-manager.test.ts +23 -0
  122. package/src/lib/server/session-run-manager.ts +11 -1
  123. package/src/lib/server/session-tools/canvas.ts +85 -50
  124. package/src/lib/server/session-tools/chatroom.ts +130 -127
  125. package/src/lib/server/session-tools/connector.ts +233 -454
  126. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  127. package/src/lib/server/session-tools/crud.ts +84 -7
  128. package/src/lib/server/session-tools/delegate.ts +351 -752
  129. package/src/lib/server/session-tools/discovery.ts +198 -0
  130. package/src/lib/server/session-tools/edit_file.ts +82 -0
  131. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  132. package/src/lib/server/session-tools/file.ts +257 -425
  133. package/src/lib/server/session-tools/git.ts +87 -47
  134. package/src/lib/server/session-tools/http.ts +85 -33
  135. package/src/lib/server/session-tools/index.ts +205 -160
  136. package/src/lib/server/session-tools/memory.ts +152 -265
  137. package/src/lib/server/session-tools/monitor.ts +126 -0
  138. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  139. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  140. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  141. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  142. package/src/lib/server/session-tools/platform.ts +86 -0
  143. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  144. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  145. package/src/lib/server/session-tools/sandbox.ts +175 -148
  146. package/src/lib/server/session-tools/schedule.ts +66 -31
  147. package/src/lib/server/session-tools/session-info.ts +104 -410
  148. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  149. package/src/lib/server/session-tools/shell.ts +171 -143
  150. package/src/lib/server/session-tools/subagent.ts +77 -77
  151. package/src/lib/server/session-tools/wallet.ts +182 -106
  152. package/src/lib/server/session-tools/web.ts +179 -349
  153. package/src/lib/server/storage.ts +24 -0
  154. package/src/lib/server/stream-agent-chat.ts +301 -244
  155. package/src/lib/server/task-quality-gate.test.ts +44 -0
  156. package/src/lib/server/task-quality-gate.ts +67 -0
  157. package/src/lib/server/task-validation.test.ts +78 -0
  158. package/src/lib/server/task-validation.ts +67 -2
  159. package/src/lib/server/tool-aliases.ts +68 -0
  160. package/src/lib/server/tool-capability-policy.ts +23 -5
  161. package/src/lib/tasks.ts +7 -1
  162. package/src/lib/tool-definitions.ts +23 -23
  163. package/src/lib/validation/schemas.ts +12 -0
  164. package/src/lib/view-routes.ts +2 -24
  165. package/src/stores/use-app-store.ts +23 -1
  166. package/src/types/index.ts +121 -7
@@ -1,35 +1,86 @@
1
1
  import { NextResponse } from 'next/server'
2
- export const dynamic = 'force-dynamic'
2
+ import { searchClawHub } from '@/lib/server/clawhub-client'
3
3
 
4
+ export const dynamic = 'force-dynamic'
4
5
 
5
- const REGISTRY_URL = 'https://swarmclaw.ai/registry/plugins.json'
6
+ const REGISTRY_URLS = [
7
+ 'https://raw.githubusercontent.com/swarmclawai/swarmforge/main/registry.json',
8
+ 'https://swarmclaw.ai/registry/plugins.json',
9
+ ]
6
10
  const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
7
11
 
8
- let cache: { data: any; fetchedAt: number } | null = null
12
+ let cache: { data: unknown; fetchedAt: number } | null = null
9
13
 
10
- export async function GET(_req: Request) {
11
- const now = Date.now()
14
+ function normalizeRegistryPluginUrl(url: unknown): string | null {
15
+ if (typeof url !== 'string') return null
16
+ const trimmed = url.trim()
17
+ if (!trimmed) return null
18
+ return trimmed
19
+ .replace('github.com/swarmclawai/plugins/', 'github.com/swarmclawai/swarmforge/')
20
+ .replace('raw.githubusercontent.com/swarmclawai/plugins/', 'raw.githubusercontent.com/swarmclawai/swarmforge/')
21
+ .replace('/swarmclawai/swarmforge/master/', '/swarmclawai/swarmforge/main/')
22
+ .replace('/swarmclawai/plugins/master/', '/swarmclawai/swarmforge/main/')
23
+ .replace('/swarmclawai/plugins/main/', '/swarmclawai/swarmforge/main/')
24
+ }
12
25
 
13
- if (cache && now - cache.fetchedAt < CACHE_TTL) {
26
+ export async function GET(req: Request) {
27
+ const { searchParams } = new URL(req.url)
28
+ const query = searchParams.get('q') || ''
29
+
30
+ const now = Date.now()
31
+ if (!query && cache && now - cache.fetchedAt < CACHE_TTL) {
14
32
  return NextResponse.json(cache.data)
15
33
  }
16
34
 
17
- try {
18
- const res = await fetch(REGISTRY_URL, { cache: 'no-store' })
19
- if (!res.ok) {
20
- throw new Error(`Registry returned ${res.status}`)
21
- }
22
- const data = await res.json()
23
- cache = { data, fetchedAt: now }
24
- return NextResponse.json(data)
25
- } catch (err: any) {
26
- // Return stale cache if available
27
- if (cache) {
28
- return NextResponse.json(cache.data)
35
+ const allPlugins: Record<string, unknown>[] = []
36
+
37
+ // 1. Fetch SwarmClaw Registry
38
+ for (const registryUrl of REGISTRY_URLS) {
39
+ try {
40
+ const res = await fetch(registryUrl, { cache: 'no-store' })
41
+ if (!res.ok) continue
42
+
43
+ const data = await res.json()
44
+ const filtered = (data as Array<{ name: string; description: string; url?: string }>).filter((p) => {
45
+ if (!p || typeof p.name !== 'string' || typeof p.description !== 'string') return false
46
+ return !query || p.name.toLowerCase().includes(query.toLowerCase()) || p.description.toLowerCase().includes(query.toLowerCase())
47
+ })
48
+
49
+ allPlugins.push(...filtered.map((p: { id?: string; name?: string; url?: string }) => ({
50
+ ...p,
51
+ id: p.id || (p.name || '').toLowerCase().replace(/[^a-z0-9]/g, '_'),
52
+ url: normalizeRegistryPluginUrl(p.url) || p.url,
53
+ source: 'swarmclaw',
54
+ })))
55
+ break
56
+ } catch (err: unknown) {
57
+ console.warn('[marketplace] SC Registry failed:', {
58
+ registryUrl,
59
+ error: err instanceof Error ? err.message : String(err),
60
+ })
29
61
  }
30
- return NextResponse.json(
31
- { error: 'Failed to fetch plugin registry', message: err.message },
32
- { status: 502 },
33
- )
34
62
  }
63
+
64
+ // 2. Fetch ClawHub Skills/Plugins
65
+ try {
66
+ const hubResults = await searchClawHub(query)
67
+ allPlugins.push(...hubResults.skills.map(s => ({
68
+ id: s.id, // Explicitly ensure ID is present
69
+ name: s.name,
70
+ description: s.description,
71
+ author: s.author,
72
+ version: s.version || '1.0.0',
73
+ url: s.url,
74
+ source: 'clawhub'
75
+ })))
76
+ } catch (err: unknown) {
77
+ console.warn('[marketplace] ClawHub failed:', err instanceof Error ? err.message : String(err))
78
+ }
79
+
80
+ // Update cache only for empty queries
81
+ if (!query) {
82
+ cache = { data: allPlugins, fetchedAt: now }
83
+ }
84
+
85
+ return NextResponse.json(allPlugins)
35
86
  }
@@ -1,7 +1,33 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { getPluginManager } from '@/lib/server/plugins'
3
- export const dynamic = 'force-dynamic'
4
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'
29
+
30
+ export const dynamic = 'force-dynamic'
5
31
 
6
32
  export async function GET(_req: Request) {
7
33
  const manager = getPluginManager()
@@ -21,3 +47,37 @@ export async function POST(req: Request) {
21
47
 
22
48
  return NextResponse.json({ ok: true })
23
49
  }
50
+
51
+ export async function DELETE(req: Request) {
52
+ const { searchParams } = new URL(req.url)
53
+ const filename = searchParams.get('filename')
54
+ if (!filename) {
55
+ return NextResponse.json({ error: 'filename required' }, { status: 400 })
56
+ }
57
+ const manager = getPluginManager()
58
+ const deleted = manager.deletePlugin(filename)
59
+ if (!deleted) {
60
+ return NextResponse.json({ error: 'Cannot delete built-in or non-existent plugin' }, { status: 400 })
61
+ }
62
+ return NextResponse.json({ ok: true })
63
+ }
64
+
65
+ export async function PATCH(req: Request) {
66
+ const { searchParams } = new URL(req.url)
67
+ const id = searchParams.get('id')
68
+ const all = searchParams.get('all') === 'true'
69
+
70
+ const manager = getPluginManager()
71
+
72
+ if (all) {
73
+ await manager.updateAllPlugins()
74
+ return NextResponse.json({ ok: true, message: 'All plugins updated' })
75
+ }
76
+
77
+ if (id) {
78
+ await manager.updatePlugin(id)
79
+ return NextResponse.json({ ok: true, message: `Plugin ${id} updated` })
80
+ }
81
+
82
+ return NextResponse.json({ error: 'id or all=true required' }, { status: 400 })
83
+ }
@@ -0,0 +1,34 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getPluginManager } from '@/lib/server/plugins'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export async function GET(req: Request) {
7
+ const { searchParams } = new URL(req.url)
8
+ const type = searchParams.get('type') // 'sidebar', 'header', etc.
9
+
10
+ const manager = getPluginManager()
11
+ const extensions = manager.getUIExtensions()
12
+
13
+ if (type === 'sidebar') {
14
+ const items = extensions.flatMap(ui => ui.sidebarItems || [])
15
+ return NextResponse.json(items)
16
+ }
17
+
18
+ if (type === 'header') {
19
+ const widgets = extensions.flatMap(ui => ui.headerWidgets || [])
20
+ return NextResponse.json(widgets)
21
+ }
22
+
23
+ if (type === 'chat_actions') {
24
+ const actions = extensions.flatMap(ui => ui.chatInputActions || [])
25
+ return NextResponse.json(actions)
26
+ }
27
+
28
+ if (type === 'connectors') {
29
+ const connectors = manager.getConnectors()
30
+ return NextResponse.json(connectors)
31
+ }
32
+
33
+ return NextResponse.json(extensions)
34
+ }
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadSettings, saveSettings } from '@/lib/server/storage'
3
+ import { DEFAULT_DELEGATION_MAX_DEPTH } from '@/lib/runtime-loop'
3
4
  export const dynamic = 'force-dynamic'
4
5
 
5
6
 
@@ -9,6 +10,16 @@ const MEMORY_PER_LOOKUP_MIN = 1
9
10
  const MEMORY_PER_LOOKUP_MAX = 200
10
11
  const MEMORY_LINKED_MIN = 0
11
12
  const MEMORY_LINKED_MAX = 1000
13
+ const DELEGATION_DEPTH_MIN = 1
14
+ const DELEGATION_DEPTH_MAX = 12
15
+ const RESPONSE_CACHE_TTL_MIN_SEC = 5
16
+ const RESPONSE_CACHE_TTL_MAX_SEC = 7 * 24 * 3600
17
+ const RESPONSE_CACHE_MAX_ENTRIES_MIN = 1
18
+ const RESPONSE_CACHE_MAX_ENTRIES_MAX = 20_000
19
+ const TASK_QG_MIN_RESULT_MIN = 10
20
+ const TASK_QG_MIN_RESULT_MAX = 2000
21
+ const TASK_QG_MIN_EVIDENCE_MIN = 0
22
+ const TASK_QG_MIN_EVIDENCE_MAX = 8
12
23
 
13
24
  function parseIntSetting(value: unknown, fallback: number, min: number, max: number): number {
14
25
  const parsed = typeof value === 'number'
@@ -20,6 +31,16 @@ function parseIntSetting(value: unknown, fallback: number, min: number, max: num
20
31
  return Math.max(min, Math.min(max, Math.trunc(parsed)))
21
32
  }
22
33
 
34
+ function parseBoolSetting(value: unknown, fallback: boolean): boolean {
35
+ if (typeof value === 'boolean') return value
36
+ if (typeof value === 'string') {
37
+ const normalized = value.trim().toLowerCase()
38
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
39
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false
40
+ }
41
+ return fallback
42
+ }
43
+
23
44
  export async function GET(_req: Request) {
24
45
  return NextResponse.json(loadSettings())
25
46
  }
@@ -47,6 +68,36 @@ export async function PUT(req: Request) {
47
68
  MEMORY_LINKED_MIN,
48
69
  MEMORY_LINKED_MAX,
49
70
  )
71
+ const nextDelegationDepth = parseIntSetting(
72
+ settings.delegationMaxDepth,
73
+ DEFAULT_DELEGATION_MAX_DEPTH,
74
+ DELEGATION_DEPTH_MIN,
75
+ DELEGATION_DEPTH_MAX,
76
+ )
77
+ const nextResponseCacheTtlSec = parseIntSetting(
78
+ settings.responseCacheTtlSec,
79
+ 15 * 60,
80
+ RESPONSE_CACHE_TTL_MIN_SEC,
81
+ RESPONSE_CACHE_TTL_MAX_SEC,
82
+ )
83
+ const nextResponseCacheMaxEntries = parseIntSetting(
84
+ settings.responseCacheMaxEntries,
85
+ 500,
86
+ RESPONSE_CACHE_MAX_ENTRIES_MIN,
87
+ RESPONSE_CACHE_MAX_ENTRIES_MAX,
88
+ )
89
+ const nextTaskQgMinResultChars = parseIntSetting(
90
+ settings.taskQualityGateMinResultChars,
91
+ 80,
92
+ TASK_QG_MIN_RESULT_MIN,
93
+ TASK_QG_MIN_RESULT_MAX,
94
+ )
95
+ const nextTaskQgMinEvidenceItems = parseIntSetting(
96
+ settings.taskQualityGateMinEvidenceItems,
97
+ 2,
98
+ TASK_QG_MIN_EVIDENCE_MIN,
99
+ TASK_QG_MIN_EVIDENCE_MAX,
100
+ )
50
101
 
51
102
  // Keep new and legacy keys synchronized for backward compatibility.
52
103
  settings.memoryReferenceDepth = nextDepth
@@ -54,6 +105,17 @@ export async function PUT(req: Request) {
54
105
  settings.maxMemoriesPerLookup = nextPerLookup
55
106
  settings.memoryMaxPerLookup = nextPerLookup
56
107
  settings.maxLinkedMemoriesExpanded = nextLinked
108
+ settings.delegationMaxDepth = nextDelegationDepth
109
+ settings.responseCacheTtlSec = nextResponseCacheTtlSec
110
+ settings.responseCacheMaxEntries = nextResponseCacheMaxEntries
111
+ settings.responseCacheEnabled = parseBoolSetting(settings.responseCacheEnabled, true)
112
+ settings.taskQualityGateEnabled = parseBoolSetting(settings.taskQualityGateEnabled, true)
113
+ settings.taskQualityGateMinResultChars = nextTaskQgMinResultChars
114
+ settings.taskQualityGateMinEvidenceItems = nextTaskQgMinEvidenceItems
115
+ settings.taskQualityGateRequireVerification = parseBoolSetting(settings.taskQualityGateRequireVerification, false)
116
+ settings.taskQualityGateRequireArtifact = parseBoolSetting(settings.taskQualityGateRequireArtifact, false)
117
+ settings.taskQualityGateRequireReport = parseBoolSetting(settings.taskQualityGateRequireReport, false)
118
+ settings.integrityMonitorEnabled = parseBoolSetting(settings.integrityMonitorEnabled, true)
57
119
 
58
120
  saveSettings(settings)
59
121
 
@@ -37,8 +37,9 @@ function run(command: string, args: string[], timeoutMs = 8_000): CommandResult
37
37
  return { ok: false, output: '', error: err || `exit ${result.status}` }
38
38
  }
39
39
  return { ok: true, output: (result.stdout || '').trim() }
40
- } catch (err: any) {
41
- return { ok: false, output: '', error: err?.message || String(err) }
40
+ } catch (err: unknown) {
41
+ const message = err instanceof Error ? err.message : String(err)
42
+ return { ok: false, output: '', error: message }
42
43
  }
43
44
  }
44
45
 
@@ -75,8 +76,9 @@ function testDataWriteAccess(dataDir: string): { ok: boolean; error?: string } {
75
76
  fs.writeFileSync(probe, 'ok', 'utf8')
76
77
  fs.unlinkSync(probe)
77
78
  return { ok: true }
78
- } catch (err: any) {
79
- return { ok: false, error: err?.message || String(err) }
79
+ } catch (err: unknown) {
80
+ const message = err instanceof Error ? err.message : String(err)
81
+ return { ok: false, error: message }
80
82
  }
81
83
  }
82
84
 
@@ -104,6 +106,22 @@ export async function GET(req: Request) {
104
106
  actions.push('Install npm and rerun `npm run setup:easy`.')
105
107
  }
106
108
 
109
+ const denoCheck = run('deno', ['--version'], 5_000)
110
+ if (denoCheck.ok) {
111
+ const denoVersion = denoCheck.output.split('\n').map((line) => line.trim()).find(Boolean) || denoCheck.output
112
+ pushCheck(checks, 'deno', 'Deno (sandbox runtime)', 'pass', `${denoVersion} is available.`, true)
113
+ } else {
114
+ pushCheck(
115
+ checks,
116
+ 'deno',
117
+ 'Deno (sandbox runtime)',
118
+ 'fail',
119
+ denoCheck.error || 'Deno was not found in PATH.',
120
+ true,
121
+ )
122
+ actions.push('Run `npm run setup:easy` to install Deno automatically, or install Deno from https://deno.land/#installation.')
123
+ }
124
+
107
125
  const dataDir = path.join(process.cwd(), 'data')
108
126
  const dataWrite = testDataWriteAccess(dataDir)
109
127
  if (dataWrite.ok) {
@@ -165,7 +183,6 @@ export async function GET(req: Request) {
165
183
  { id: 'claude-cli', label: 'Claude Code CLI', command: 'claude' },
166
184
  { id: 'codex-cli', label: 'OpenAI Codex CLI', command: 'codex' },
167
185
  { id: 'opencode-cli', label: 'OpenCode CLI', command: 'opencode' },
168
- { id: 'deno', label: 'Deno (sandbox runtime)', command: 'deno' },
169
186
  ]
170
187
 
171
188
  for (const binary of optionalBinaries) {
@@ -58,11 +58,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
58
58
  saveTasks(t2)
59
59
  notify('tasks')
60
60
  }
61
- } catch (err: any) {
62
- console.error(`[approve] Resume failed for task ${id}:`, err.message)
61
+ } catch (err: unknown) {
62
+ const errMsg = err instanceof Error ? err.message : String(err)
63
+ console.error(`[approve] Resume failed for task ${id}:`, errMsg)
63
64
  const t2 = loadTasks()
64
65
  if (t2[id]) {
65
- t2[id].error = err.message || String(err)
66
+ t2[id].error = errMsg
66
67
  t2[id].updatedAt = Date.now()
67
68
  saveTasks(t2)
68
69
  notify('tasks')
@@ -1,8 +1,8 @@
1
1
  import { genId } from '@/lib/id'
2
2
  import { NextResponse } from 'next/server'
3
- import { loadTasks, saveTasks, logActivity } from '@/lib/server/storage'
3
+ import { loadTasks, saveTasks, logActivity, loadSettings } from '@/lib/server/storage'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
- import { disableSessionHeartbeat, enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
5
+ import { disableSessionHeartbeat, enqueueTask, recoverStalledRunningTasks, validateCompletedTasksQueue } from '@/lib/server/queue'
6
6
  import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
7
7
  import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
8
8
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
@@ -12,10 +12,12 @@ import { enqueueSystemEvent } from '@/lib/server/system-events'
12
12
  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
+ import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
15
16
 
16
17
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
17
18
  // Keep completed queue integrity even if daemon is not running.
18
19
  validateCompletedTasksQueue()
20
+ recoverStalledRunningTasks()
19
21
 
20
22
  const { id } = await params
21
23
  const tasks = loadTasks()
@@ -26,6 +28,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
26
28
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
27
29
  const { id } = await params
28
30
  const body = await req.json()
31
+ const settings = loadSettings()
29
32
  const tasks = loadTasks()
30
33
  if (!tasks[id]) return notFound()
31
34
 
@@ -48,6 +51,11 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
48
51
  tasks[id].comments.push(body.appendComment)
49
52
  tasks[id].updatedAt = Date.now()
50
53
  } else {
54
+ if (Object.prototype.hasOwnProperty.call(body, 'qualityGate')) {
55
+ body.qualityGate = body.qualityGate
56
+ ? normalizeTaskQualityGate(body.qualityGate, settings)
57
+ : null
58
+ }
51
59
  Object.assign(tasks[id], body, { updatedAt: Date.now() })
52
60
  // Explicitly clear nullable fields when sent as null (Object.assign copies null but not undefined)
53
61
  if (body.projectId === null) delete tasks[id].projectId
@@ -63,7 +71,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
63
71
  if (tasks[id].status === 'completed') {
64
72
  const report = ensureTaskCompletionReport(tasks[id])
65
73
  if (report?.relativePath) tasks[id].completionReportPath = report.relativePath
66
- const validation = validateTaskCompletion(tasks[id], { report })
74
+ const validation = validateTaskCompletion(tasks[id], { report, settings })
67
75
  tasks[id].validation = validation
68
76
  if (validation.ok) {
69
77
  tasks[id].completedAt = tasks[id].completedAt || Date.now()
@@ -3,7 +3,7 @@ import { genId } from '@/lib/id'
3
3
  import { loadTasks, saveTasks, loadSettings, loadAgents, logActivity } from '@/lib/server/storage'
4
4
  import { TaskCreateSchema, formatZodError } from '@/lib/validation/schemas'
5
5
  import { z } from 'zod'
6
- import { enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
6
+ import { enqueueTask, recoverStalledRunningTasks, validateCompletedTasksQueue } from '@/lib/server/queue'
7
7
  import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
8
8
  import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
9
9
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
@@ -12,10 +12,12 @@ import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
12
12
  import { resolveTaskAgentFromDescription } from '@/lib/server/task-mention'
13
13
  import { validateDag } from '@/lib/server/dag-validation'
14
14
  import { getPluginManager } from '@/lib/server/plugins'
15
+ import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
15
16
 
16
17
  export async function GET(req: Request) {
17
18
  // Keep completed queue integrity even if daemon is not running.
18
19
  validateCompletedTasksQueue()
20
+ recoverStalledRunningTasks()
19
21
 
20
22
  const { searchParams } = new URL(req.url)
21
23
  const includeArchived = searchParams.get('includeArchived') === 'true'
@@ -69,6 +71,9 @@ export async function POST(req: Request) {
69
71
  const now = Date.now()
70
72
  const tasks = loadTasks()
71
73
  const settings = loadSettings()
74
+ const normalizedQualityGate = body.qualityGate
75
+ ? normalizeTaskQualityGate(body.qualityGate, settings)
76
+ : null
72
77
  const maxAttempts = Number.isFinite(Number(body.maxAttempts))
73
78
  ? Math.max(1, Math.min(20, Math.trunc(Number(body.maxAttempts))))
74
79
  : Math.max(1, Math.min(20, Math.trunc(Number(settings.defaultTaskMaxAttempts ?? 3))))
@@ -147,6 +152,7 @@ export async function POST(req: Request) {
147
152
  customFields: body.customFields && typeof body.customFields === 'object' ? body.customFields : undefined,
148
153
  priority: ['low', 'medium', 'high', 'critical'].includes(body.priority) ? body.priority : undefined,
149
154
  fingerprint: computeTaskFingerprint(body.title || 'Untitled Task', body.agentId || ''),
155
+ qualityGate: normalizedQualityGate,
150
156
  }
151
157
 
152
158
  // Dedup: if a non-terminal task with same fingerprint exists, return it
@@ -158,7 +164,7 @@ export async function POST(req: Request) {
158
164
  if (tasks[id].status === 'completed') {
159
165
  const report = ensureTaskCompletionReport(tasks[id])
160
166
  if (report?.relativePath) tasks[id].completionReportPath = report.relativePath
161
- const validation = validateTaskCompletion(tasks[id], { report })
167
+ const validation = validateTaskCompletion(tasks[id], { report, settings })
162
168
  tasks[id].validation = validation
163
169
  if (validation.ok) {
164
170
  tasks[id].completedAt = Date.now()
@@ -171,6 +171,12 @@ textarea::-webkit-scrollbar { width: 0; }
171
171
  textarea:hover { scrollbar-width: thin; }
172
172
  textarea:hover::-webkit-scrollbar { width: 6px; }
173
173
 
174
+ /* Improve scroll behavior on iOS/iPadOS nested panes */
175
+ .overflow-y-auto,
176
+ .overflow-auto {
177
+ -webkit-overflow-scrolling: touch;
178
+ }
179
+
174
180
  /* Selection */
175
181
  ::selection { background: rgba(99,102,241,0.3); }
176
182
 
@@ -250,6 +256,23 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
250
256
  0% { background-position: -200% center; }
251
257
  100% { background-position: 200% center; }
252
258
  }
259
+ @keyframes shimmer-bar {
260
+ 0% { transform: translateX(-100%); }
261
+ 100% { transform: translateX(100%); }
262
+ }
263
+ @keyframes spring-in {
264
+ 0% { transform: scale(0.9) translateY(10px); opacity: 0; }
265
+ 70% { transform: scale(1.02) translateY(-2px); opacity: 1; }
266
+ 100% { transform: scale(1) translateY(0); opacity: 1; }
267
+ }
268
+ @keyframes pulse-subtle {
269
+ 0%, 100% { opacity: 1; transform: scale(1); }
270
+ 50% { opacity: 0.8; transform: scale(1.02); }
271
+ }
272
+ @keyframes glow-line {
273
+ 0% { left: -100%; }
274
+ 100% { left: 100%; }
275
+ }
253
276
  @keyframes gradient-drift {
254
277
  0% { background-position: 0% 50%; }
255
278
  50% { background-position: 100% 50%; }
@@ -259,6 +282,10 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
259
282
  from { opacity: 0; transform: translateY(10px); }
260
283
  to { opacity: 1; transform: translateY(0); }
261
284
  }
285
+ @keyframes fade-up {
286
+ from { opacity: 0; transform: translateY(10px); }
287
+ to { opacity: 1; transform: translateY(0); }
288
+ }
262
289
 
263
290
  /* Heartbeat float animation */
264
291
  @keyframes heartbeat-float {
package/src/app/page.tsx CHANGED
@@ -92,14 +92,14 @@ function FullScreenLoader() {
92
92
  background: 'linear-gradient(135deg, rgba(255,255,255,0.6), rgba(129, 140, 248, 0.8))',
93
93
  WebkitBackgroundClip: 'text',
94
94
  WebkitTextFillColor: 'transparent',
95
- animation: 'sc-text-fade 2s ease-in-out infinite alternate',
95
+ animation: 'sc-text-fade 2s ease-in-out infinite alternate, fade-up 0.6s var(--ease-spring) 0.2s both',
96
96
  }}
97
97
  >
98
98
  SwarmClaw
99
99
  </div>
100
100
 
101
101
  {/* Loading bar */}
102
- <div className="mt-4 w-[100px] h-[2px] rounded-full bg-white/[0.06] overflow-hidden">
102
+ <div className="mt-4 w-[100px] h-[2px] rounded-full bg-white/[0.06] overflow-hidden" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
103
103
  <div
104
104
  className="h-full rounded-full bg-accent-bright/60"
105
105
  style={{ animation: 'sc-progress 1.5s ease-in-out infinite' }}
@@ -150,7 +150,10 @@ export default function Home() {
150
150
 
151
151
  const [authChecked, setAuthChecked] = useState(false)
152
152
  const [authenticated, setAuthenticated] = useState(false)
153
- const [setupDone, setSetupDone] = useState<boolean | null>(null)
153
+ const [setupDone, setSetupDone] = useState<boolean | null>(() => {
154
+ if (typeof window !== 'undefined' && localStorage.getItem('sc_setup_done') === '1') return true
155
+ return null
156
+ })
154
157
 
155
158
  const checkAuth = useCallback(async () => {
156
159
  const key = getStoredAccessKey()
@@ -252,7 +255,9 @@ export default function Home() {
252
255
  ])
253
256
  if (cancelled) return
254
257
  const hasCreds = Object.keys(creds).length > 0
255
- setSetupDone(settings.setupCompleted === true || hasCreds)
258
+ const done = settings.setupCompleted === true || hasCreds
259
+ if (done) localStorage.setItem('sc_setup_done', '1')
260
+ setSetupDone(done)
256
261
  } catch {
257
262
  if (!cancelled) setSetupDone(true) // on error, skip wizard
258
263
  }
@@ -285,6 +290,6 @@ export default function Home() {
285
290
  if (!authenticated) return <AccessKeyGate onAuthenticated={() => setAuthenticated(true)} />
286
291
  if (!currentUser) return <UserPicker />
287
292
  if (setupDone === null || !agentReady) return <FullScreenLoader />
288
- if (!setupDone) return <SetupWizard onComplete={() => setSetupDone(true)} />
293
+ if (!setupDone) return <SetupWizard onComplete={() => { localStorage.setItem('sc_setup_done', '1'); setSetupDone(true) }} />
289
294
  return <AppLayout />
290
295
  }
package/src/cli/index.js CHANGED
@@ -42,6 +42,14 @@ const COMMAND_GROUPS = [
42
42
  }),
43
43
  ],
44
44
  },
45
+ {
46
+ name: 'approvals',
47
+ description: 'Manage runtime approvals',
48
+ commands: [
49
+ cmd('list', 'GET', '/approvals', 'List pending approvals'),
50
+ cmd('resolve', 'POST', '/approvals', 'Approve/reject a pending approval', { expectsJsonBody: true }),
51
+ ],
52
+ },
45
53
  {
46
54
  name: 'claude-skills',
47
55
  description: 'Read local Claude skills directory metadata',
@@ -251,6 +259,8 @@ const COMMAND_GROUPS = [
251
259
  cmd('delete', 'DELETE', '/mcp-servers/:id', 'Delete MCP server'),
252
260
  cmd('test', 'POST', '/mcp-servers/:id/test', 'Test MCP server connection'),
253
261
  cmd('tools', 'GET', '/mcp-servers/:id/tools', 'List tools available on an MCP server'),
262
+ cmd('conformance', 'POST', '/mcp-servers/:id/conformance', 'Run MCP conformance checks for a server', { expectsJsonBody: true }),
263
+ cmd('invoke', 'POST', '/mcp-servers/:id/invoke', 'Invoke an MCP tool on a server', { expectsJsonBody: true }),
254
264
  ],
255
265
  },
256
266
  {
@@ -331,8 +341,11 @@ const COMMAND_GROUPS = [
331
341
  commands: [
332
342
  cmd('list', 'GET', '/plugins', 'List installed plugins'),
333
343
  cmd('set', 'POST', '/plugins', 'Enable/disable plugin', { expectsJsonBody: true }),
344
+ cmd('delete', 'DELETE', '/plugins', 'Delete an external plugin (use --query filename=plugin.js)'),
345
+ cmd('update', 'PATCH', '/plugins', 'Update a plugin (use --query id=plugin.js or --query all=true)'),
334
346
  cmd('install', 'POST', '/plugins/install', 'Install plugin from URL', { expectsJsonBody: true }),
335
347
  cmd('marketplace', 'GET', '/plugins/marketplace', 'Get marketplace catalog'),
348
+ cmd('ui', 'GET', '/plugins/ui', 'List plugin UI extensions (use --query type=sidebar|header|chat_actions|connectors)'),
336
349
  ],
337
350
  },
338
351
  {
@@ -65,8 +65,15 @@ export function ActivityFeed() {
65
65
  <div className="text-center text-text-3 text-[14px] mt-16">No activity yet</div>
66
66
  ) : (
67
67
  <div className="space-y-1">
68
- {entries.map((entry: ActivityEntry) => (
69
- <div key={entry.id} className="flex items-start gap-3 py-3 border-b border-white/[0.04]">
68
+ {entries.map((entry: ActivityEntry, idx: number) => (
69
+ <div
70
+ key={entry.id}
71
+ className="flex items-start gap-3 py-3 border-b border-white/[0.04]"
72
+ style={{
73
+ animation: 'fade-up 0.5s var(--ease-spring) both',
74
+ animationDelay: `${Math.min(idx * 0.03, 0.5)}s`
75
+ }}
76
+ >
70
77
  <div className="w-8 h-8 rounded-[8px] bg-surface-2 flex items-center justify-center text-[12px] font-700 text-text-3 shrink-0">
71
78
  {ENTITY_ICONS[entry.entityType] || '?'}
72
79
  </div>
@@ -38,7 +38,11 @@ export function AgentAvatar({ seed, avatarUrl, name, size = 32, className = '',
38
38
  const dot = status && status !== 'idle' ? (
39
39
  <span
40
40
  className={`absolute -bottom-0.5 -right-0.5 rounded-full ${STATUS_COLORS[status]} ring-2 ring-[#0f0f1a]`}
41
- style={{ width: dotSize, height: dotSize }}
41
+ style={{
42
+ width: dotSize,
43
+ height: dotSize,
44
+ animation: status === 'online' ? 'pulse-subtle 2s ease-in-out infinite' : undefined
45
+ }}
42
46
  title={status === 'busy' ? 'Busy' : 'Online'}
43
47
  />
44
48
  ) : null