@swarmclawai/swarmclaw 0.4.5 → 0.5.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 (113) hide show
  1. package/README.md +8 -2
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +5 -0
  4. package/package.json +2 -1
  5. package/src/app/api/agents/[id]/route.ts +23 -5
  6. package/src/app/api/agents/trash/route.ts +44 -0
  7. package/src/app/api/connectors/[id]/route.ts +7 -4
  8. package/src/app/api/connectors/[id]/webhook/route.ts +6 -2
  9. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  10. package/src/app/api/openclaw/approvals/route.ts +46 -0
  11. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  12. package/src/app/api/openclaw/cron/route.ts +52 -0
  13. package/src/app/api/openclaw/directory/route.ts +4 -3
  14. package/src/app/api/openclaw/discover/route.ts +3 -2
  15. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  16. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  17. package/src/app/api/openclaw/gateway/route.ts +72 -0
  18. package/src/app/api/openclaw/history/route.ts +109 -0
  19. package/src/app/api/openclaw/media/route.ts +53 -0
  20. package/src/app/api/openclaw/models/route.ts +12 -0
  21. package/src/app/api/openclaw/permissions/route.ts +39 -0
  22. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  23. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  24. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  25. package/src/app/api/openclaw/skills/route.ts +82 -0
  26. package/src/app/api/openclaw/sync/route.ts +3 -2
  27. package/src/app/api/projects/[id]/route.ts +1 -1
  28. package/src/app/api/projects/route.ts +1 -1
  29. package/src/app/api/secrets/[id]/route.ts +1 -1
  30. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  31. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  32. package/src/app/api/sessions/[id]/messages/route.ts +18 -1
  33. package/src/app/api/sessions/[id]/route.ts +12 -3
  34. package/src/app/api/sessions/route.ts +6 -2
  35. package/src/app/globals.css +14 -0
  36. package/src/app/layout.tsx +5 -20
  37. package/src/cli/index.js +33 -1
  38. package/src/cli/spec.js +40 -0
  39. package/src/components/agents/agent-avatar.tsx +45 -0
  40. package/src/components/agents/agent-card.tsx +19 -5
  41. package/src/components/agents/agent-chat-list.tsx +31 -24
  42. package/src/components/agents/agent-files-editor.tsx +185 -0
  43. package/src/components/agents/agent-list.tsx +82 -3
  44. package/src/components/agents/agent-sheet.tsx +31 -0
  45. package/src/components/agents/cron-job-form.tsx +137 -0
  46. package/src/components/agents/exec-config-panel.tsx +147 -0
  47. package/src/components/agents/inspector-panel.tsx +310 -0
  48. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  49. package/src/components/agents/permission-preset-selector.tsx +79 -0
  50. package/src/components/agents/personality-builder.tsx +111 -0
  51. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  52. package/src/components/agents/skill-install-dialog.tsx +102 -0
  53. package/src/components/agents/trash-list.tsx +109 -0
  54. package/src/components/chat/chat-area.tsx +14 -2
  55. package/src/components/chat/chat-header.tsx +168 -4
  56. package/src/components/chat/chat-preview-panel.tsx +113 -0
  57. package/src/components/chat/exec-approval-card.tsx +89 -0
  58. package/src/components/chat/message-bubble.tsx +218 -36
  59. package/src/components/chat/message-list.tsx +135 -31
  60. package/src/components/chat/streaming-bubble.tsx +59 -10
  61. package/src/components/chat/suggestions-bar.tsx +74 -0
  62. package/src/components/chat/thinking-indicator.tsx +20 -6
  63. package/src/components/chat/tool-call-bubble.tsx +89 -16
  64. package/src/components/chat/tool-request-banner.tsx +20 -2
  65. package/src/components/chat/trace-block.tsx +103 -0
  66. package/src/components/projects/project-list.tsx +1 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  68. package/src/components/shared/avatar.tsx +13 -2
  69. package/src/components/shared/settings/settings-page.tsx +1 -0
  70. package/src/components/tasks/task-board.tsx +1 -1
  71. package/src/components/tasks/task-sheet.tsx +12 -12
  72. package/src/hooks/use-continuous-speech.ts +42 -5
  73. package/src/hooks/use-openclaw-gateway.ts +63 -0
  74. package/src/lib/notification-sounds.ts +58 -0
  75. package/src/lib/personality-parser.ts +97 -0
  76. package/src/lib/providers/openclaw.ts +17 -2
  77. package/src/lib/runtime-loop.ts +2 -2
  78. package/src/lib/server/chat-execution.ts +44 -2
  79. package/src/lib/server/connectors/bluebubbles.test.ts +17 -8
  80. package/src/lib/server/connectors/bluebubbles.ts +5 -2
  81. package/src/lib/server/connectors/googlechat.ts +14 -10
  82. package/src/lib/server/connectors/manager.ts +37 -15
  83. package/src/lib/server/connectors/openclaw.ts +1 -0
  84. package/src/lib/server/daemon-state.ts +11 -0
  85. package/src/lib/server/main-agent-loop.ts +2 -3
  86. package/src/lib/server/main-session.ts +21 -0
  87. package/src/lib/server/openclaw-config-sync.ts +107 -0
  88. package/src/lib/server/openclaw-exec-config.ts +52 -0
  89. package/src/lib/server/openclaw-gateway.ts +291 -0
  90. package/src/lib/server/openclaw-history-merge.ts +36 -0
  91. package/src/lib/server/openclaw-models.ts +56 -0
  92. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  93. package/src/lib/server/openclaw-sync.ts +11 -10
  94. package/src/lib/server/queue.ts +2 -1
  95. package/src/lib/server/session-tools/connector.ts +4 -4
  96. package/src/lib/server/session-tools/delegate.ts +19 -3
  97. package/src/lib/server/session-tools/file.ts +20 -20
  98. package/src/lib/server/session-tools/index.ts +2 -2
  99. package/src/lib/server/session-tools/openclaw-nodes.ts +6 -6
  100. package/src/lib/server/session-tools/sandbox.ts +2 -2
  101. package/src/lib/server/session-tools/search-providers.ts +13 -6
  102. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  103. package/src/lib/server/session-tools/shell.ts +1 -1
  104. package/src/lib/server/session-tools/web.ts +8 -8
  105. package/src/lib/server/storage.ts +62 -11
  106. package/src/lib/server/stream-agent-chat.ts +24 -4
  107. package/src/lib/server/suggestions.ts +20 -0
  108. package/src/lib/server/ws-hub.ts +14 -0
  109. package/src/stores/use-app-store.ts +53 -1
  110. package/src/stores/use-approval-store.ts +78 -0
  111. package/src/stores/use-chat-store.ts +153 -5
  112. package/src/types/index.ts +127 -1
  113. package/tsconfig.json +13 -4
@@ -0,0 +1,72 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected, getGateway, disconnectGateway, manualConnect } from '@/lib/server/openclaw-gateway'
3
+
4
+ /** POST — proxy an RPC call or perform gateway actions */
5
+ export async function POST(req: Request) {
6
+ const body = await req.json()
7
+ const { method, params } = body as { method?: string; params?: Record<string, unknown> }
8
+ if (!method || typeof method !== 'string') {
9
+ return NextResponse.json({ error: 'Missing RPC method' }, { status: 400 })
10
+ }
11
+
12
+ // Gateway control actions
13
+ if (method === 'gateway.connect') {
14
+ try {
15
+ const url = (params?.url as string) || undefined
16
+ const token = (params?.token as string) || undefined
17
+ const ok = await manualConnect(url, token)
18
+ return NextResponse.json({ ok })
19
+ } catch (err: unknown) {
20
+ return NextResponse.json({ ok: false, error: err instanceof Error ? err.message : String(err) }, { status: 502 })
21
+ }
22
+ }
23
+
24
+ if (method === 'gateway.disconnect') {
25
+ disconnectGateway()
26
+ return NextResponse.json({ ok: true })
27
+ }
28
+
29
+ // Reload mode get/set
30
+ if (method === 'gateway.reload-mode.get') {
31
+ const gw = await ensureGatewayConnected()
32
+ if (!gw) return NextResponse.json({ error: 'Not connected' }, { status: 503 })
33
+ try {
34
+ const config = await gw.rpc('config.get') as Record<string, unknown> | undefined
35
+ const mode = (config as Record<string, unknown>)?.reloadMode ?? 'hot'
36
+ return NextResponse.json({ ok: true, result: mode })
37
+ } catch (err: unknown) {
38
+ return NextResponse.json({ error: err instanceof Error ? err.message : String(err) }, { status: 502 })
39
+ }
40
+ }
41
+
42
+ if (method === 'gateway.reload-mode.set') {
43
+ const gw = await ensureGatewayConnected()
44
+ if (!gw) return NextResponse.json({ error: 'Not connected' }, { status: 503 })
45
+ try {
46
+ await gw.rpc('config.set', { reloadMode: params?.mode })
47
+ return NextResponse.json({ ok: true })
48
+ } catch (err: unknown) {
49
+ return NextResponse.json({ error: err instanceof Error ? err.message : String(err) }, { status: 502 })
50
+ }
51
+ }
52
+
53
+ // General RPC proxy
54
+ const gw = await ensureGatewayConnected()
55
+ if (!gw) {
56
+ return NextResponse.json({ error: 'OpenClaw gateway not connected' }, { status: 503 })
57
+ }
58
+
59
+ try {
60
+ const result = await gw.rpc(method, params)
61
+ return NextResponse.json({ ok: true, result })
62
+ } catch (err: unknown) {
63
+ const message = err instanceof Error ? err.message : String(err)
64
+ return NextResponse.json({ error: message }, { status: 502 })
65
+ }
66
+ }
67
+
68
+ /** GET — check gateway connection status */
69
+ export async function GET() {
70
+ const gw = getGateway()
71
+ return NextResponse.json({ connected: !!gw?.connected })
72
+ }
@@ -0,0 +1,109 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+ import { mergeHistoryMessages, isValidSessionKey } from '@/lib/server/openclaw-history-merge'
4
+ import { loadSessions, saveSessions } from '@/lib/server/storage'
5
+ import { notify } from '@/lib/server/ws-hub'
6
+ import type { GatewaySessionPreview } from '@/types'
7
+
8
+ /**
9
+ * Extract a single session preview from the gateway response.
10
+ * The gateway may return:
11
+ * - A map: { [sessionKey]: preview }
12
+ * - An array: [preview, ...]
13
+ * - A single object with sessionKey field
14
+ */
15
+ function extractPreview(
16
+ raw: unknown,
17
+ sessionKey: string,
18
+ ): GatewaySessionPreview | undefined {
19
+ if (!raw || typeof raw !== 'object') return undefined
20
+
21
+ // Direct object with messages array
22
+ if ('messages' in (raw as Record<string, unknown>)) {
23
+ return raw as GatewaySessionPreview
24
+ }
25
+
26
+ // Map keyed by session key
27
+ const asMap = raw as Record<string, unknown>
28
+ if (asMap[sessionKey] && typeof asMap[sessionKey] === 'object') {
29
+ return asMap[sessionKey] as GatewaySessionPreview
30
+ }
31
+
32
+ // Array — find matching session
33
+ if (Array.isArray(raw)) {
34
+ return raw.find(
35
+ (p: unknown) =>
36
+ p && typeof p === 'object' && (p as GatewaySessionPreview).sessionKey === sessionKey,
37
+ ) as GatewaySessionPreview | undefined
38
+ }
39
+
40
+ return undefined
41
+ }
42
+
43
+ /** GET ?sessionKey=X — preview gateway session history */
44
+ export async function GET(req: Request) {
45
+ const { searchParams } = new URL(req.url)
46
+ const sessionKey = searchParams.get('sessionKey')
47
+ if (!sessionKey || !isValidSessionKey(sessionKey)) {
48
+ return NextResponse.json({ error: 'Missing or invalid sessionKey' }, { status: 400 })
49
+ }
50
+
51
+ const gw = await ensureGatewayConnected()
52
+ if (!gw) {
53
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
54
+ }
55
+
56
+ try {
57
+ const raw = await gw.rpc('sessions.preview', { keys: [sessionKey], limit: 100 })
58
+ const preview = extractPreview(raw, sessionKey)
59
+ return NextResponse.json(preview ?? { sessionKey, epoch: 0, messages: [] })
60
+ } catch (err: unknown) {
61
+ const message = err instanceof Error ? err.message : String(err)
62
+ return NextResponse.json({ error: message }, { status: 502 })
63
+ }
64
+ }
65
+
66
+ /** POST { sessionKey, epoch, localSessionId } — merge gateway history into local session */
67
+ export async function POST(req: Request) {
68
+ const body = await req.json()
69
+ const { sessionKey, localSessionId } = body as {
70
+ sessionKey?: string
71
+ epoch?: number
72
+ localSessionId?: string
73
+ }
74
+ if (!sessionKey || !localSessionId) {
75
+ return NextResponse.json({ error: 'Missing sessionKey or localSessionId' }, { status: 400 })
76
+ }
77
+
78
+ const gw = await ensureGatewayConnected()
79
+ if (!gw) {
80
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
81
+ }
82
+
83
+ try {
84
+ const raw = await gw.rpc('sessions.preview', { keys: [sessionKey], limit: 100 })
85
+ const preview = extractPreview(raw, sessionKey)
86
+ if (!preview?.messages?.length) {
87
+ return NextResponse.json({ ok: true, merged: 0 })
88
+ }
89
+
90
+ const sessions = loadSessions()
91
+ const session = sessions[localSessionId]
92
+ if (!session) {
93
+ return NextResponse.json({ error: 'Local session not found' }, { status: 404 })
94
+ }
95
+
96
+ const merged = mergeHistoryMessages(session.messages, preview)
97
+ const newCount = merged.length - session.messages.length
98
+ session.messages = merged
99
+ session.lastActiveAt = Date.now()
100
+ sessions[localSessionId] = session
101
+ saveSessions(sessions)
102
+ notify('sessions')
103
+
104
+ return NextResponse.json({ ok: true, merged: newCount })
105
+ } catch (err: unknown) {
106
+ const message = err instanceof Error ? err.message : String(err)
107
+ return NextResponse.json({ error: message }, { status: 502 })
108
+ }
109
+ }
@@ -0,0 +1,53 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+ import { lookup } from 'mime-types'
4
+
5
+ const MAX_SIZE = 25 * 1024 * 1024 // 25MB
6
+
7
+ /** GET ?path=... — proxy agent files (images etc.) from gateway */
8
+ export async function GET(req: Request) {
9
+ const { searchParams } = new URL(req.url)
10
+ const filePath = searchParams.get('path')
11
+ if (!filePath) {
12
+ return NextResponse.json({ error: 'Missing path' }, { status: 400 })
13
+ }
14
+
15
+ // Security: path must be under ~/.openclaw/
16
+ if (!filePath.includes('.openclaw') && !filePath.includes('.clawdbot')) {
17
+ return NextResponse.json({ error: 'Path not allowed' }, { status: 403 })
18
+ }
19
+
20
+ const gw = await ensureGatewayConnected()
21
+ if (!gw) {
22
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
23
+ }
24
+
25
+ try {
26
+ const result = await gw.rpc('files.read', { path: filePath }) as { content?: string; encoding?: string } | undefined
27
+ if (!result?.content) {
28
+ return NextResponse.json({ error: 'File not found' }, { status: 404 })
29
+ }
30
+
31
+ const isBase64 = result.encoding === 'base64'
32
+ const buf = isBase64
33
+ ? Buffer.from(result.content, 'base64')
34
+ : Buffer.from(result.content, 'utf-8')
35
+
36
+ if (buf.length > MAX_SIZE) {
37
+ return NextResponse.json({ error: 'File too large' }, { status: 413 })
38
+ }
39
+
40
+ const mimeType = lookup(filePath) || 'application/octet-stream'
41
+
42
+ return new NextResponse(buf, {
43
+ headers: {
44
+ 'Content-Type': mimeType,
45
+ 'Content-Length': String(buf.length),
46
+ 'Cache-Control': 'public, max-age=300',
47
+ },
48
+ })
49
+ } catch (err: unknown) {
50
+ const message = err instanceof Error ? err.message : String(err)
51
+ return NextResponse.json({ error: message }, { status: 502 })
52
+ }
53
+ }
@@ -0,0 +1,12 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { fetchGatewayModelPolicy, buildAllowedModelKeys } from '@/lib/server/openclaw-models'
3
+
4
+ /** GET — fetch allowed models for OpenClaw agents from gateway policy */
5
+ export async function GET() {
6
+ const policy = await fetchGatewayModelPolicy()
7
+ const models = buildAllowedModelKeys(policy)
8
+ return NextResponse.json({
9
+ models: models ?? ['default'],
10
+ defaultModel: policy?.defaultModel ?? 'default',
11
+ })
12
+ }
@@ -0,0 +1,39 @@
1
+ import { NextResponse } from 'next/server'
2
+ import type { PermissionPreset } from '@/types'
3
+ import { getExecConfig } from '@/lib/server/openclaw-exec-config'
4
+ import { resolvePresetFromConfig, applyPreset } from '@/lib/server/openclaw-permission-presets'
5
+
6
+ /** GET ?agentId=X — resolve current permission preset */
7
+ export async function GET(req: Request) {
8
+ const { searchParams } = new URL(req.url)
9
+ const agentId = searchParams.get('agentId')
10
+ if (!agentId) {
11
+ return NextResponse.json({ error: 'Missing agentId' }, { status: 400 })
12
+ }
13
+
14
+ try {
15
+ const snap = await getExecConfig(agentId)
16
+ const preset = resolvePresetFromConfig(snap.file)
17
+ return NextResponse.json({ preset, config: snap.file })
18
+ } catch (err: unknown) {
19
+ const message = err instanceof Error ? err.message : String(err)
20
+ return NextResponse.json({ error: message }, { status: 502 })
21
+ }
22
+ }
23
+
24
+ /** PUT { agentId, preset } — apply a permission preset */
25
+ export async function PUT(req: Request) {
26
+ const body = await req.json()
27
+ const { agentId, preset } = body as { agentId?: string; preset?: PermissionPreset }
28
+ if (!agentId || !preset) {
29
+ return NextResponse.json({ error: 'Missing agentId or preset' }, { status: 400 })
30
+ }
31
+
32
+ try {
33
+ await applyPreset(agentId, preset)
34
+ return NextResponse.json({ ok: true })
35
+ } catch (err: unknown) {
36
+ const message = err instanceof Error ? err.message : String(err)
37
+ return NextResponse.json({ error: message }, { status: 502 })
38
+ }
39
+ }
@@ -0,0 +1,69 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+
4
+ /** GET — list available and allowed env keys for sandbox */
5
+ export async function GET() {
6
+ const gw = await ensureGatewayConnected()
7
+ if (!gw) {
8
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
9
+ }
10
+
11
+ try {
12
+ // Get available keys from dotenv
13
+ const available = await gw.rpc('env.keys') as string[] | undefined
14
+
15
+ // Get current config to find allowed keys
16
+ const config = await gw.rpc('config.get') as Record<string, unknown> | undefined
17
+ const agents = (config as Record<string, unknown>)?.agents as Record<string, unknown> | undefined
18
+ const defaults = agents?.defaults as Record<string, unknown> | undefined
19
+ const sandbox = defaults?.sandbox as Record<string, unknown> | undefined
20
+ const docker = sandbox?.docker as Record<string, unknown> | undefined
21
+ const envList = docker?.env as string[] | undefined
22
+
23
+ // Parse allowed keys from ${KEY} format
24
+ const allowed = (envList ?? [])
25
+ .map((entry) => {
26
+ const m = entry.match(/^\$\{(.+)\}$/)
27
+ return m ? m[1] : entry
28
+ })
29
+
30
+ return NextResponse.json({ available: available ?? [], allowed })
31
+ } catch (err: unknown) {
32
+ const message = err instanceof Error ? err.message : String(err)
33
+ return NextResponse.json({ error: message }, { status: 502 })
34
+ }
35
+ }
36
+
37
+ /** PUT { allowed: string[] } — update sandbox env allowlist */
38
+ export async function PUT(req: Request) {
39
+ const body = await req.json()
40
+ const { allowed } = body as { allowed?: string[] }
41
+ if (!allowed || !Array.isArray(allowed)) {
42
+ return NextResponse.json({ error: 'Missing allowed array' }, { status: 400 })
43
+ }
44
+
45
+ const gw = await ensureGatewayConnected()
46
+ if (!gw) {
47
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
48
+ }
49
+
50
+ try {
51
+ // Format as ${KEY} for gateway config
52
+ const envEntries = allowed.map((key) => `\${${key}}`)
53
+
54
+ // Fetch current config hash for conflict detection
55
+ const config = await gw.rpc('config.get') as Record<string, unknown> | undefined
56
+ const configHash = (config as Record<string, unknown>)?._hash as string | undefined
57
+
58
+ await gw.rpc('config.set', {
59
+ key: 'agents.defaults.sandbox.docker.env',
60
+ value: envEntries,
61
+ baseHash: configHash,
62
+ })
63
+
64
+ return NextResponse.json({ ok: true })
65
+ } catch (err: unknown) {
66
+ const message = err instanceof Error ? err.message : String(err)
67
+ return NextResponse.json({ error: message }, { status: 502 })
68
+ }
69
+ }
@@ -0,0 +1,32 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+
4
+ /** POST { name, installId, timeoutMs? } — install a skill via gateway */
5
+ export async function POST(req: Request) {
6
+ const body = await req.json()
7
+ const { name, installId, timeoutMs } = body as {
8
+ name?: string
9
+ installId?: string
10
+ timeoutMs?: number
11
+ }
12
+ if (!name) {
13
+ return NextResponse.json({ error: 'Missing skill name' }, { status: 400 })
14
+ }
15
+
16
+ const gw = await ensureGatewayConnected()
17
+ if (!gw) {
18
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
19
+ }
20
+
21
+ try {
22
+ const result = await gw.rpc('skills.install', {
23
+ name,
24
+ installId,
25
+ timeoutMs: timeoutMs ?? 120_000,
26
+ }, (timeoutMs ?? 120_000) + 5_000)
27
+ return NextResponse.json({ ok: true, result })
28
+ } catch (err: unknown) {
29
+ const message = err instanceof Error ? err.message : String(err)
30
+ return NextResponse.json({ error: message }, { status: 502 })
31
+ }
32
+ }
@@ -0,0 +1,24 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+
4
+ /** POST { skillKey, source } — remove a skill via gateway */
5
+ export async function POST(req: Request) {
6
+ const body = await req.json()
7
+ const { skillKey, source } = body as { skillKey?: string; source?: string }
8
+ if (!skillKey) {
9
+ return NextResponse.json({ error: 'Missing skillKey' }, { status: 400 })
10
+ }
11
+
12
+ const gw = await ensureGatewayConnected()
13
+ if (!gw) {
14
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
15
+ }
16
+
17
+ try {
18
+ await gw.rpc('skills.remove', { skillKey, source })
19
+ return NextResponse.json({ ok: true })
20
+ } catch (err: unknown) {
21
+ const message = err instanceof Error ? err.message : String(err)
22
+ return NextResponse.json({ error: message }, { status: 502 })
23
+ }
24
+ }
@@ -0,0 +1,82 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+ import { loadAgents, saveAgents } from '@/lib/server/storage'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+ import type { OpenClawSkillEntry, SkillAllowlistMode } from '@/types'
6
+
7
+ /** GET ?agentId=X — fetch skills from gateway with eligibility */
8
+ export async function GET(req: Request) {
9
+ const { searchParams } = new URL(req.url)
10
+ const agentId = searchParams.get('agentId')
11
+ if (!agentId) {
12
+ return NextResponse.json({ error: 'Missing agentId' }, { status: 400 })
13
+ }
14
+
15
+ const gw = await ensureGatewayConnected()
16
+ if (!gw) {
17
+ return NextResponse.json({ error: 'OpenClaw gateway not connected' }, { status: 503 })
18
+ }
19
+
20
+ try {
21
+ const result = await gw.rpc('skills.status', { agentId }) as OpenClawSkillEntry[] | undefined
22
+ return NextResponse.json(result ?? [])
23
+ } catch (err: unknown) {
24
+ const message = err instanceof Error ? err.message : String(err)
25
+ return NextResponse.json({ error: message }, { status: 502 })
26
+ }
27
+ }
28
+
29
+ /** PATCH { skillKey, enabled?, apiKey? } — update a skill's config on gateway */
30
+ export async function PATCH(req: Request) {
31
+ const body = await req.json()
32
+ const { skillKey, enabled, apiKey } = body as {
33
+ skillKey?: string
34
+ enabled?: boolean
35
+ apiKey?: string
36
+ }
37
+ if (!skillKey) {
38
+ return NextResponse.json({ error: 'Missing skillKey' }, { status: 400 })
39
+ }
40
+
41
+ const gw = await ensureGatewayConnected()
42
+ if (!gw) {
43
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
44
+ }
45
+
46
+ try {
47
+ await gw.rpc('skills.update', { skillKey, enabled, apiKey })
48
+ return NextResponse.json({ ok: true })
49
+ } catch (err: unknown) {
50
+ const message = err instanceof Error ? err.message : String(err)
51
+ return NextResponse.json({ error: message }, { status: 502 })
52
+ }
53
+ }
54
+
55
+ /** PUT { agentId, mode, allowedSkills } — save skill allowlist config to agent */
56
+ export async function PUT(req: Request) {
57
+ const body = await req.json()
58
+ const { agentId, mode, allowedSkills } = body as {
59
+ agentId?: string
60
+ mode?: SkillAllowlistMode
61
+ allowedSkills?: string[]
62
+ }
63
+
64
+ if (!agentId || !mode) {
65
+ return NextResponse.json({ error: 'Missing agentId or mode' }, { status: 400 })
66
+ }
67
+
68
+ const agents = loadAgents({ includeTrashed: true })
69
+ const agent = agents[agentId]
70
+ if (!agent) {
71
+ return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
72
+ }
73
+
74
+ agent.openclawSkillMode = mode
75
+ agent.openclawAllowedSkills = mode === 'selected' ? (allowedSkills ?? []) : undefined
76
+ agent.updatedAt = Date.now()
77
+ agents[agentId] = agent
78
+ saveAgents(agents)
79
+ notify('agents')
80
+
81
+ return NextResponse.json({ ok: true })
82
+ }
@@ -24,7 +24,8 @@ export async function POST(req: Request) {
24
24
 
25
25
  const results = await runSync({ action, types: validTypes })
26
26
  return NextResponse.json({ ok: true, results })
27
- } catch (err: any) {
28
- return NextResponse.json({ error: err.message || 'Sync failed' }, { status: 500 })
27
+ } catch (err: unknown) {
28
+ const message = err instanceof Error ? err.message : 'Sync failed'
29
+ return NextResponse.json({ error: message }, { status: 500 })
29
30
  }
30
31
  }
@@ -31,7 +31,7 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
31
31
  if (!deleteItem(ops, id)) return notFound()
32
32
 
33
33
  // Clear projectId from referencing entities
34
- const clearProjectId = (load: () => Record<string, any>, save: (d: Record<string, any>) => void, topic: string) => {
34
+ const clearProjectId = (load: () => Record<string, Record<string, unknown>>, save: (d: Record<string, Record<string, unknown>>) => void, topic: string) => {
35
35
  const items = load()
36
36
  let changed = false
37
37
  for (const item of Object.values(items)) {
@@ -4,7 +4,7 @@ import { loadProjects, saveProjects } from '@/lib/server/storage'
4
4
  import { notify } from '@/lib/server/ws-hub'
5
5
  export const dynamic = 'force-dynamic'
6
6
 
7
- export async function GET(_req: Request) {
7
+ export async function GET() {
8
8
  return NextResponse.json(loadProjects())
9
9
  }
10
10
 
@@ -23,6 +23,6 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
23
23
  return secret
24
24
  })
25
25
  if (!result) return notFound()
26
- const { encryptedValue, ...safe } = result as Record<string, any>
26
+ const { encryptedValue, ...safe } = result as Record<string, unknown>
27
27
  return NextResponse.json(safe)
28
28
  }
@@ -0,0 +1,22 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadSessions, saveSessions } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
4
+
5
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
6
+ const { id } = await params
7
+ const body = await req.json() as { messageIndex: number; newText: string }
8
+ const sessions = loadSessions()
9
+ const session = sessions[id]
10
+ if (!session) return notFound()
11
+
12
+ const { messageIndex, newText } = body
13
+ if (typeof messageIndex !== 'number' || messageIndex < 0 || messageIndex >= session.messages.length) {
14
+ return NextResponse.json({ error: 'Invalid message index' }, { status: 400 })
15
+ }
16
+
17
+ // Truncate messages to messageIndex (discard that msg + everything after)
18
+ session.messages = session.messages.slice(0, messageIndex)
19
+ saveSessions(sessions)
20
+
21
+ return NextResponse.json({ message: newText })
22
+ }
@@ -0,0 +1,44 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { randomUUID } from 'node:crypto'
3
+ import { loadSessions, saveSessions } from '@/lib/server/storage'
4
+ import { notFound } from '@/lib/server/collection-helpers'
5
+
6
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
7
+ const { id } = await params
8
+ const body = await req.json() as { messageIndex: number }
9
+ const sessions = loadSessions()
10
+ const source = sessions[id]
11
+ if (!source) return notFound()
12
+
13
+ const { messageIndex } = body
14
+ if (typeof messageIndex !== 'number' || messageIndex < 0 || messageIndex >= source.messages.length) {
15
+ return NextResponse.json({ error: 'Invalid message index' }, { status: 400 })
16
+ }
17
+
18
+ const now = Date.now()
19
+ const newId = randomUUID()
20
+ const forked = {
21
+ id: newId,
22
+ name: `Fork of ${source.name}`,
23
+ cwd: source.cwd,
24
+ user: source.user,
25
+ provider: source.provider,
26
+ model: source.model,
27
+ credentialId: source.credentialId ?? null,
28
+ fallbackCredentialIds: source.fallbackCredentialIds,
29
+ apiEndpoint: source.apiEndpoint ?? null,
30
+ claudeSessionId: null,
31
+ messages: source.messages.slice(0, messageIndex + 1),
32
+ createdAt: now,
33
+ lastActiveAt: now,
34
+ agentId: source.agentId ?? null,
35
+ parentSessionId: id,
36
+ tools: source.tools,
37
+ conversationTone: source.conversationTone,
38
+ }
39
+
40
+ sessions[newId] = forked
41
+ saveSessions(sessions)
42
+
43
+ return NextResponse.json(forked)
44
+ }
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadSessions } from '@/lib/server/storage'
2
+ import { loadSessions, saveSessions } from '@/lib/server/storage'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
4
 
5
5
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -8,3 +8,20 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
8
8
  if (!sessions[id]) return notFound()
9
9
  return NextResponse.json(sessions[id].messages)
10
10
  }
11
+
12
+ export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
13
+ const { id } = await params
14
+ const body = await req.json() as { messageIndex: number; bookmarked: boolean }
15
+ const sessions = loadSessions()
16
+ const session = sessions[id]
17
+ if (!session) return notFound()
18
+
19
+ const { messageIndex, bookmarked } = body
20
+ if (typeof messageIndex !== 'number' || messageIndex < 0 || messageIndex >= session.messages.length) {
21
+ return NextResponse.json({ error: 'Invalid message index' }, { status: 400 })
22
+ }
23
+
24
+ session.messages[messageIndex].bookmarked = bookmarked
25
+ saveSessions(sessions)
26
+ return NextResponse.json(session.messages[messageIndex])
27
+ }