@swarmclawai/swarmclaw 0.5.3 → 0.6.2
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.
- package/README.md +53 -9
- package/bin/server-cmd.js +1 -0
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +5 -2
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/route.ts +4 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +53 -1
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +12 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +18 -2
- package/src/app/api/tts/route.ts +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +63 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +40 -1
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +50 -17
- package/src/components/agents/agent-chat-list.tsx +148 -12
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +120 -65
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +173 -0
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +457 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +146 -0
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +356 -315
- package/src/components/chat/message-list.tsx +230 -8
- package/src/components/chat/streaming-bubble.tsx +104 -47
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +111 -73
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +130 -0
- package/src/components/chatrooms/chatroom-message.tsx +432 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +95 -56
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +107 -43
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +194 -97
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +1 -1
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +259 -126
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +44 -6
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +31 -15
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +57 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +182 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +59 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +416 -74
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-view-router.ts +69 -19
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +75 -15
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +229 -7
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +14 -2
- package/src/lib/server/daemon-state.ts +157 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +48 -6
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +105 -0
- package/src/lib/server/memory-db.ts +183 -10
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +56 -10
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +10 -0
- package/src/lib/server/session-tools/memory.ts +7 -1
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +115 -4
- package/src/lib/server/storage.ts +53 -29
- package/src/lib/server/stream-agent-chat.ts +185 -57
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/stores/use-app-store.ts +41 -3
- package/src/stores/use-chat-store.ts +113 -5
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +88 -4
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadTasks, loadAgents } from '@/lib/server/storage'
|
|
3
|
+
|
|
4
|
+
type Range = '24h' | '7d' | '30d'
|
|
5
|
+
|
|
6
|
+
const RANGE_MS: Record<Range, number> = {
|
|
7
|
+
'24h': 24 * 3600_000,
|
|
8
|
+
'7d': 7 * 86400_000,
|
|
9
|
+
'30d': 30 * 86400_000,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function bucketKey(ts: number, range: Range): string {
|
|
13
|
+
const d = new Date(ts)
|
|
14
|
+
if (range === '24h') return d.toISOString().slice(0, 13) // "2026-03-01T14"
|
|
15
|
+
return d.toISOString().slice(0, 10) // "2026-03-01"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function GET(req: Request) {
|
|
19
|
+
const { searchParams } = new URL(req.url)
|
|
20
|
+
const range = (searchParams.get('range') as Range) || '7d'
|
|
21
|
+
const cutoff = Date.now() - (RANGE_MS[range] || RANGE_MS['7d'])
|
|
22
|
+
|
|
23
|
+
const tasks = loadTasks()
|
|
24
|
+
const agents = loadAgents()
|
|
25
|
+
const all = Object.values(tasks)
|
|
26
|
+
|
|
27
|
+
// --- by-status counts ---
|
|
28
|
+
const byStatus: Record<string, number> = {}
|
|
29
|
+
for (const t of all) {
|
|
30
|
+
byStatus[t.status] = (byStatus[t.status] || 0) + 1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// WIP = queued + running
|
|
34
|
+
const wip = (byStatus['queued'] || 0) + (byStatus['running'] || 0)
|
|
35
|
+
|
|
36
|
+
// --- completions in range ---
|
|
37
|
+
const completedInRange = all.filter(
|
|
38
|
+
(t) => t.status === 'completed' && t.completedAt && t.completedAt >= cutoff,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// --- cycle times (queuedAt → completedAt) ---
|
|
42
|
+
const cycleTimes: number[] = []
|
|
43
|
+
for (const t of completedInRange) {
|
|
44
|
+
const start = t.queuedAt || t.createdAt
|
|
45
|
+
const end = t.completedAt!
|
|
46
|
+
if (end > start) cycleTimes.push(end - start)
|
|
47
|
+
}
|
|
48
|
+
cycleTimes.sort((a, b) => a - b)
|
|
49
|
+
|
|
50
|
+
const avgCycleMs = cycleTimes.length
|
|
51
|
+
? Math.round(cycleTimes.reduce((s, v) => s + v, 0) / cycleTimes.length)
|
|
52
|
+
: 0
|
|
53
|
+
const p50CycleMs = cycleTimes.length ? cycleTimes[Math.floor(cycleTimes.length * 0.5)] : 0
|
|
54
|
+
const p90CycleMs = cycleTimes.length ? cycleTimes[Math.floor(cycleTimes.length * 0.9)] : 0
|
|
55
|
+
|
|
56
|
+
// --- velocity (completions per bucket) ---
|
|
57
|
+
const velocityMap: Record<string, number> = {}
|
|
58
|
+
for (const t of completedInRange) {
|
|
59
|
+
const key = bucketKey(t.completedAt!, range)
|
|
60
|
+
velocityMap[key] = (velocityMap[key] || 0) + 1
|
|
61
|
+
}
|
|
62
|
+
const velocity = Object.entries(velocityMap)
|
|
63
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
64
|
+
.map(([bucket, count]) => ({ bucket, count }))
|
|
65
|
+
|
|
66
|
+
// --- by-agent completions ---
|
|
67
|
+
const byAgent: Record<string, { agentName: string; completed: number; failed: number }> = {}
|
|
68
|
+
const recentTasks = all.filter(
|
|
69
|
+
(t) => (t.completedAt && t.completedAt >= cutoff) || (t.status === 'failed' && t.updatedAt >= cutoff),
|
|
70
|
+
)
|
|
71
|
+
for (const t of recentTasks) {
|
|
72
|
+
if (!t.agentId) continue
|
|
73
|
+
if (!byAgent[t.agentId]) {
|
|
74
|
+
const agent = agents[t.agentId]
|
|
75
|
+
byAgent[t.agentId] = { agentName: agent?.name || t.agentId, completed: 0, failed: 0 }
|
|
76
|
+
}
|
|
77
|
+
if (t.status === 'completed') byAgent[t.agentId].completed++
|
|
78
|
+
else if (t.status === 'failed') byAgent[t.agentId].failed++
|
|
79
|
+
}
|
|
80
|
+
const byAgentList = Object.values(byAgent).sort((a, b) => b.completed - a.completed)
|
|
81
|
+
|
|
82
|
+
// --- by-priority counts ---
|
|
83
|
+
const byPriority: Record<string, number> = {}
|
|
84
|
+
for (const t of all) {
|
|
85
|
+
const p = t.priority || 'none'
|
|
86
|
+
byPriority[p] = (byPriority[p] || 0) + 1
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return NextResponse.json({
|
|
90
|
+
range,
|
|
91
|
+
byStatus,
|
|
92
|
+
wip,
|
|
93
|
+
completedCount: completedInRange.length,
|
|
94
|
+
avgCycleMs,
|
|
95
|
+
p50CycleMs,
|
|
96
|
+
p90CycleMs,
|
|
97
|
+
velocity,
|
|
98
|
+
byAgent: byAgentList,
|
|
99
|
+
byPriority,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
|
-
import { loadTasks, saveTasks, loadSettings, logActivity } from '@/lib/server/storage'
|
|
3
|
+
import { loadTasks, saveTasks, loadSettings, loadAgents, logActivity } from '@/lib/server/storage'
|
|
4
4
|
import { enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
|
|
5
5
|
import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
|
|
6
6
|
import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
|
|
7
7
|
import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
|
|
8
8
|
import { notify } from '@/lib/server/ws-hub'
|
|
9
|
+
import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
|
|
10
|
+
import { resolveTaskAgentFromDescription } from '@/lib/server/task-mention'
|
|
9
11
|
|
|
10
12
|
export async function GET(req: Request) {
|
|
11
13
|
// Keep completed queue integrity even if daemon is not running.
|
|
@@ -64,12 +66,18 @@ export async function POST(req: Request) {
|
|
|
64
66
|
const retryBackoffSec = Number.isFinite(Number(body.retryBackoffSec))
|
|
65
67
|
? Math.max(1, Math.min(3600, Math.trunc(Number(body.retryBackoffSec))))
|
|
66
68
|
: Math.max(1, Math.min(3600, Math.trunc(Number(settings.taskRetryBackoffSec ?? 30))))
|
|
69
|
+
// Resolve @mentions in description to auto-assign agent
|
|
70
|
+
const resolvedAgentId = body.description
|
|
71
|
+
? resolveTaskAgentFromDescription(body.description, body.agentId || '', loadAgents())
|
|
72
|
+
: (body.agentId || '')
|
|
73
|
+
|
|
67
74
|
tasks[id] = {
|
|
68
75
|
id,
|
|
69
76
|
title: body.title || 'Untitled Task',
|
|
70
77
|
description: body.description || '',
|
|
71
78
|
status: body.status || 'backlog',
|
|
72
|
-
agentId:
|
|
79
|
+
agentId: resolvedAgentId,
|
|
80
|
+
projectId: typeof body.projectId === 'string' && body.projectId ? body.projectId : null,
|
|
73
81
|
goalContract: body.goalContract || null,
|
|
74
82
|
cwd: typeof body.cwd === 'string' ? body.cwd : null,
|
|
75
83
|
file: typeof body.file === 'string' ? body.file : null,
|
|
@@ -93,6 +101,14 @@ export async function POST(req: Request) {
|
|
|
93
101
|
tags: Array.isArray(body.tags) ? body.tags.filter((s: unknown) => typeof s === 'string') : [],
|
|
94
102
|
dueAt: typeof body.dueAt === 'number' ? body.dueAt : null,
|
|
95
103
|
customFields: body.customFields && typeof body.customFields === 'object' ? body.customFields : undefined,
|
|
104
|
+
priority: ['low', 'medium', 'high', 'critical'].includes(body.priority) ? body.priority : undefined,
|
|
105
|
+
fingerprint: computeTaskFingerprint(body.title || 'Untitled Task', body.agentId || ''),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Dedup: if a non-terminal task with same fingerprint exists, return it
|
|
109
|
+
const dupe = findDuplicateTask(tasks, { fingerprint: tasks[id].fingerprint! })
|
|
110
|
+
if (dupe && dupe.id !== id) {
|
|
111
|
+
return NextResponse.json({ ...dupe, deduplicated: true })
|
|
96
112
|
}
|
|
97
113
|
|
|
98
114
|
if (tasks[id].status === 'completed') {
|
package/src/app/api/tts/route.ts
CHANGED
|
@@ -10,8 +10,9 @@ export async function POST(req: Request) {
|
|
|
10
10
|
return new NextResponse('No ElevenLabs API key. Set one in Settings > Voice.', { status: 500 })
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const { text } = await req.json()
|
|
14
|
-
const
|
|
13
|
+
const { text, voiceId } = await req.json()
|
|
14
|
+
const voice = voiceId || ELEVENLABS_VOICE
|
|
15
|
+
const apiRes = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voice}`, {
|
|
15
16
|
method: 'POST',
|
|
16
17
|
headers: {
|
|
17
18
|
'xi-api-key': ELEVENLABS_KEY,
|
|
@@ -9,13 +9,14 @@ export async function POST(req: Request) {
|
|
|
9
9
|
return new Response('No ElevenLabs API key. Set one in Settings > Voice.', { status: 500 })
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const { text } = await req.json()
|
|
12
|
+
const { text, voiceId } = await req.json()
|
|
13
13
|
if (!text?.trim()) {
|
|
14
14
|
return new Response('No text provided', { status: 400 })
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const voice = voiceId || ELEVENLABS_VOICE
|
|
17
18
|
const apiRes = await fetch(
|
|
18
|
-
`https://api.elevenlabs.io/v1/text-to-speech/${
|
|
19
|
+
`https://api.elevenlabs.io/v1/text-to-speech/${voice}/stream`,
|
|
19
20
|
{
|
|
20
21
|
method: 'POST',
|
|
21
22
|
headers: {
|
|
@@ -3,40 +3,7 @@ import { notFound } from '@/lib/server/collection-helpers'
|
|
|
3
3
|
import fs from 'fs'
|
|
4
4
|
import path from 'path'
|
|
5
5
|
import { UPLOAD_DIR } from '@/lib/server/storage'
|
|
6
|
-
|
|
7
|
-
const MIME_TYPES: Record<string, string> = {
|
|
8
|
-
'.png': 'image/png',
|
|
9
|
-
'.jpg': 'image/jpeg',
|
|
10
|
-
'.jpeg': 'image/jpeg',
|
|
11
|
-
'.gif': 'image/gif',
|
|
12
|
-
'.webp': 'image/webp',
|
|
13
|
-
'.svg': 'image/svg+xml',
|
|
14
|
-
'.bmp': 'image/bmp',
|
|
15
|
-
'.ico': 'image/x-icon',
|
|
16
|
-
'.mp4': 'video/mp4',
|
|
17
|
-
'.webm': 'video/webm',
|
|
18
|
-
'.mov': 'video/quicktime',
|
|
19
|
-
'.avi': 'video/x-msvideo',
|
|
20
|
-
'.mkv': 'video/x-matroska',
|
|
21
|
-
'.pdf': 'application/pdf',
|
|
22
|
-
'.json': 'application/json',
|
|
23
|
-
'.csv': 'text/csv',
|
|
24
|
-
'.txt': 'text/plain',
|
|
25
|
-
'.html': 'text/html',
|
|
26
|
-
'.xml': 'application/xml',
|
|
27
|
-
'.zip': 'application/zip',
|
|
28
|
-
'.tar': 'application/x-tar',
|
|
29
|
-
'.gz': 'application/gzip',
|
|
30
|
-
'.doc': 'application/msword',
|
|
31
|
-
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
32
|
-
'.xls': 'application/vnd.ms-excel',
|
|
33
|
-
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
34
|
-
'.ppt': 'application/vnd.ms-powerpoint',
|
|
35
|
-
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
36
|
-
'.mp3': 'audio/mpeg',
|
|
37
|
-
'.wav': 'audio/wav',
|
|
38
|
-
'.ogg': 'audio/ogg',
|
|
39
|
-
}
|
|
6
|
+
import { MIME_TYPES } from '@/lib/server/mime'
|
|
40
7
|
|
|
41
8
|
export async function GET(_req: Request, { params }: { params: Promise<{ filename: string }> }) {
|
|
42
9
|
const { filename } = await params
|
|
@@ -60,3 +27,21 @@ export async function GET(_req: Request, { params }: { params: Promise<{ filenam
|
|
|
60
27
|
},
|
|
61
28
|
})
|
|
62
29
|
}
|
|
30
|
+
|
|
31
|
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ filename: string }> }) {
|
|
32
|
+
const { filename } = await params
|
|
33
|
+
const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, '')
|
|
34
|
+
|
|
35
|
+
if (safeName.includes('..') || safeName.includes('/')) {
|
|
36
|
+
return NextResponse.json({ error: 'Invalid filename' }, { status: 400 })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const filePath = path.join(UPLOAD_DIR, safeName)
|
|
40
|
+
|
|
41
|
+
if (!fs.existsSync(filePath)) {
|
|
42
|
+
return notFound()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fs.unlinkSync(filePath)
|
|
46
|
+
return NextResponse.json({ ok: true })
|
|
47
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { UPLOAD_DIR } from '@/lib/server/storage'
|
|
5
|
+
import { getFileCategory } from '@/lib/server/mime'
|
|
6
|
+
|
|
7
|
+
interface UploadFile {
|
|
8
|
+
name: string
|
|
9
|
+
size: number
|
|
10
|
+
modified: number
|
|
11
|
+
category: string
|
|
12
|
+
url: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function listUploadFiles(): UploadFile[] {
|
|
16
|
+
if (!fs.existsSync(UPLOAD_DIR)) return []
|
|
17
|
+
const entries = fs.readdirSync(UPLOAD_DIR)
|
|
18
|
+
const files: UploadFile[] = []
|
|
19
|
+
for (const name of entries) {
|
|
20
|
+
const filePath = path.join(UPLOAD_DIR, name)
|
|
21
|
+
try {
|
|
22
|
+
const stat = fs.statSync(filePath)
|
|
23
|
+
if (!stat.isFile()) continue
|
|
24
|
+
const ext = path.extname(name).toLowerCase()
|
|
25
|
+
files.push({
|
|
26
|
+
name,
|
|
27
|
+
size: stat.size,
|
|
28
|
+
modified: stat.mtimeMs,
|
|
29
|
+
category: getFileCategory(ext),
|
|
30
|
+
url: `/api/uploads/${encodeURIComponent(name)}`,
|
|
31
|
+
})
|
|
32
|
+
} catch {
|
|
33
|
+
// skip files we can't stat
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return files
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function GET() {
|
|
40
|
+
const files = listUploadFiles()
|
|
41
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0)
|
|
42
|
+
return NextResponse.json({ files, totalSize, count: files.length })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface DeleteBody {
|
|
46
|
+
filenames?: string[]
|
|
47
|
+
olderThanDays?: number
|
|
48
|
+
category?: string
|
|
49
|
+
all?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isUnsafeName(name: string): boolean {
|
|
53
|
+
return name.includes('/') || name.includes('\\') || name.includes('..')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function DELETE(req: Request) {
|
|
57
|
+
const body = (await req.json()) as DeleteBody
|
|
58
|
+
const files = listUploadFiles()
|
|
59
|
+
let toDelete: string[] = []
|
|
60
|
+
|
|
61
|
+
if (body.all) {
|
|
62
|
+
toDelete = files.map((f) => f.name)
|
|
63
|
+
} else if (body.filenames && Array.isArray(body.filenames)) {
|
|
64
|
+
for (const name of body.filenames) {
|
|
65
|
+
if (typeof name !== 'string' || isUnsafeName(name)) {
|
|
66
|
+
return NextResponse.json({ error: `Invalid filename: ${name}` }, { status: 400 })
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
toDelete = body.filenames
|
|
70
|
+
} else if (typeof body.olderThanDays === 'number') {
|
|
71
|
+
const cutoff = Date.now() - body.olderThanDays * 86_400_000
|
|
72
|
+
toDelete = files.filter((f) => f.modified < cutoff).map((f) => f.name)
|
|
73
|
+
} else if (typeof body.category === 'string') {
|
|
74
|
+
toDelete = files.filter((f) => f.category === body.category).map((f) => f.name)
|
|
75
|
+
} else {
|
|
76
|
+
return NextResponse.json({ error: 'Provide filenames, olderThanDays, category, or all' }, { status: 400 })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let deleted = 0
|
|
80
|
+
let freedBytes = 0
|
|
81
|
+
for (const name of toDelete) {
|
|
82
|
+
const filePath = path.join(UPLOAD_DIR, name)
|
|
83
|
+
try {
|
|
84
|
+
const stat = fs.statSync(filePath)
|
|
85
|
+
fs.unlinkSync(filePath)
|
|
86
|
+
freedBytes += stat.size
|
|
87
|
+
deleted++
|
|
88
|
+
} catch {
|
|
89
|
+
// file already gone or inaccessible
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return NextResponse.json({ deleted, freedBytes })
|
|
94
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { genId } from '@/lib/id'
|
|
2
|
+
import { timingSafeEqual } from 'node:crypto'
|
|
2
3
|
import { NextResponse } from 'next/server'
|
|
3
4
|
import { loadAgents, loadSessions, loadWebhooks, saveSessions, saveWebhooks, appendWebhookLog, upsertWebhookRetry } from '@/lib/server/storage'
|
|
4
5
|
import { WORKSPACE_DIR } from '@/lib/server/data-dir'
|
|
5
6
|
import { enqueueSessionRun } from '@/lib/server/session-run-manager'
|
|
7
|
+
import { enqueueSystemEvent } from '@/lib/server/system-events'
|
|
8
|
+
import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
|
|
6
9
|
import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
|
|
7
10
|
import type { WebhookRetryEntry } from '@/types'
|
|
8
11
|
|
|
@@ -71,7 +74,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
71
74
|
if (secret) {
|
|
72
75
|
const url = new URL(req.url)
|
|
73
76
|
const provided = req.headers.get('x-webhook-secret') || url.searchParams.get('secret') || ''
|
|
74
|
-
|
|
77
|
+
const secretBuf = Buffer.from(secret)
|
|
78
|
+
const providedBuf = Buffer.from(provided)
|
|
79
|
+
// timingSafeEqual requires equal lengths; compare against secretBuf if lengths differ
|
|
80
|
+
const compareBuf = providedBuf.length === secretBuf.length ? providedBuf : secretBuf
|
|
81
|
+
const isInvalid = providedBuf.length !== secretBuf.length || !timingSafeEqual(secretBuf, compareBuf)
|
|
82
|
+
if (isInvalid) {
|
|
75
83
|
appendWebhookLog(genId(8), {
|
|
76
84
|
id: genId(8), webhookId: id, event: 'unknown',
|
|
77
85
|
payload: '', status: 'error', error: 'Invalid webhook secret', timestamp: Date.now(),
|
|
@@ -193,6 +201,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
193
201
|
mode: 'followup',
|
|
194
202
|
})
|
|
195
203
|
|
|
204
|
+
// Enqueue system event + heartbeat wake
|
|
205
|
+
enqueueSystemEvent(sid, `Webhook received: ${webhook.name || id} (${incomingEvent})`)
|
|
206
|
+
if (webhook.agentId) {
|
|
207
|
+
requestHeartbeatNow({ agentId: webhook.agentId, reason: 'webhook' })
|
|
208
|
+
}
|
|
209
|
+
|
|
196
210
|
appendWebhookLog(genId(8), {
|
|
197
211
|
id: genId(8), webhookId: id, event: incomingEvent,
|
|
198
212
|
payload: (rawBody || '').slice(0, 2000), status: 'success',
|
package/src/app/globals.css
CHANGED
|
@@ -49,16 +49,17 @@
|
|
|
49
49
|
--radius-4xl: calc(var(--radius) + 16px);
|
|
50
50
|
|
|
51
51
|
/* ===== Midnight Glass Palette ===== */
|
|
52
|
-
--
|
|
53
|
-
--color-
|
|
54
|
-
--color-
|
|
55
|
-
--color-surface
|
|
56
|
-
--color-surface-
|
|
52
|
+
/* Surfaces derived from --neutral-tint for single-knob theming */
|
|
53
|
+
--color-bg: color-mix(in srgb, var(--neutral-tint, #1e1e30) 40%, #000);
|
|
54
|
+
--color-raised: color-mix(in srgb, var(--neutral-tint, #1e1e30) 50%, #000);
|
|
55
|
+
--color-surface: color-mix(in srgb, var(--neutral-tint, #1e1e30) 65%, #000);
|
|
56
|
+
--color-surface-2: var(--neutral-tint, #1e1e30);
|
|
57
|
+
--color-surface-3: color-mix(in srgb, var(--neutral-tint, #1e1e30) 80%, #333);
|
|
57
58
|
--color-border-hi: rgba(255,255,255,0.07);
|
|
58
59
|
--color-border-focus: rgba(99,102,241,0.5);
|
|
59
60
|
--color-text: #e2e2ec;
|
|
60
|
-
--color-text-2: #
|
|
61
|
-
--color-text-3: #
|
|
61
|
+
--color-text-2: #a0a0ba;
|
|
62
|
+
--color-text-3: #8282a0;
|
|
62
63
|
--color-accent-soft: rgba(99,102,241,0.08);
|
|
63
64
|
--color-accent-glow: rgba(99,102,241,0.18);
|
|
64
65
|
--color-accent-bright: #818CF8;
|
|
@@ -83,25 +84,29 @@
|
|
|
83
84
|
--font-sora: 'Segoe UI';
|
|
84
85
|
--font-jetbrains-mono: 'SF Mono';
|
|
85
86
|
--radius: 0.625rem;
|
|
86
|
-
|
|
87
|
+
|
|
88
|
+
/* ===== Single-Tint Theming ===== */
|
|
89
|
+
/* Change this one value to shift the entire palette hue */
|
|
90
|
+
--neutral-tint: #1e1e30;
|
|
91
|
+
--background: color-mix(in srgb, var(--neutral-tint) 40%, #000);
|
|
87
92
|
--foreground: #e2e2ec;
|
|
88
|
-
--card: #
|
|
93
|
+
--card: color-mix(in srgb, var(--neutral-tint) 50%, #000);
|
|
89
94
|
--card-foreground: #e2e2ec;
|
|
90
|
-
--popover: #
|
|
95
|
+
--popover: color-mix(in srgb, var(--neutral-tint) 50%, #000);
|
|
91
96
|
--popover-foreground: #e2e2ec;
|
|
92
97
|
--primary: #6366F1;
|
|
93
98
|
--primary-foreground: #ffffff;
|
|
94
|
-
--secondary: #
|
|
99
|
+
--secondary: color-mix(in srgb, var(--neutral-tint) 65%, #000);
|
|
95
100
|
--secondary-foreground: #e2e2ec;
|
|
96
|
-
--muted: #
|
|
97
|
-
--muted-foreground: #
|
|
101
|
+
--muted: color-mix(in srgb, var(--neutral-tint) 65%, #000);
|
|
102
|
+
--muted-foreground: #a0a0ba;
|
|
98
103
|
--accent: #6366F1;
|
|
99
104
|
--accent-foreground: #ffffff;
|
|
100
105
|
--destructive: #F43F5E;
|
|
101
106
|
--border: rgba(255,255,255,0.04);
|
|
102
107
|
--input: rgba(255,255,255,0.04);
|
|
103
108
|
--ring: rgba(99,102,241,0.4);
|
|
104
|
-
--sidebar: #
|
|
109
|
+
--sidebar: color-mix(in srgb, var(--neutral-tint) 50%, #000);
|
|
105
110
|
--sidebar-foreground: #e2e2ec;
|
|
106
111
|
--sidebar-primary: #6366F1;
|
|
107
112
|
--sidebar-primary-foreground: #ffffff;
|
|
@@ -113,7 +118,7 @@
|
|
|
113
118
|
/* ===== Status Badge System ===== */
|
|
114
119
|
--status-idle-bg: rgba(255,255,255,0.04);
|
|
115
120
|
--status-idle-border: rgba(255,255,255,0.06);
|
|
116
|
-
--status-idle-fg: #
|
|
121
|
+
--status-idle-fg: #a0a0ba;
|
|
117
122
|
--status-running-bg: rgba(52,211,153,0.08);
|
|
118
123
|
--status-running-border: rgba(52,211,153,0.15);
|
|
119
124
|
--status-running-fg: #34D399;
|
|
@@ -205,6 +210,10 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
|
|
|
205
210
|
from { opacity: 0; transform: scale(0.97) translateY(6px); }
|
|
206
211
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
207
212
|
}
|
|
213
|
+
@keyframes msg-in {
|
|
214
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
215
|
+
to { opacity: 1; transform: translateY(0); }
|
|
216
|
+
}
|
|
208
217
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
209
218
|
@keyframes slide-in-left {
|
|
210
219
|
from { transform: translateX(-100%); }
|
|
@@ -243,6 +252,35 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
|
|
|
243
252
|
to { opacity: 1; transform: translateY(0); }
|
|
244
253
|
}
|
|
245
254
|
|
|
255
|
+
/* Heartbeat float animation */
|
|
256
|
+
@keyframes heartbeat-float {
|
|
257
|
+
0% { opacity: 1; transform: translateY(0) scale(1); }
|
|
258
|
+
50% { opacity: 0.7; transform: translateY(-14px) scale(1.15); }
|
|
259
|
+
100% { opacity: 0; transform: translateY(-26px) scale(0.85); }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* Activity moment animations */
|
|
263
|
+
@keyframes activity-moment-in {
|
|
264
|
+
from { opacity: 0; transform: translateY(6px) scale(0.92); }
|
|
265
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
266
|
+
}
|
|
267
|
+
@keyframes activity-moment-out {
|
|
268
|
+
from { opacity: 1; transform: translateY(0) scale(1); }
|
|
269
|
+
to { opacity: 0; transform: translateY(-8px) scale(0.95); }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@keyframes delegation-handoff-in {
|
|
273
|
+
0% { opacity: 0; transform: translateX(16px) scale(0.95); }
|
|
274
|
+
100% { opacity: 1; transform: translateX(0) scale(1); }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@keyframes avatar-moment-pulse {
|
|
278
|
+
0% { transform: scale(1); }
|
|
279
|
+
30% { transform: scale(1.15); }
|
|
280
|
+
60% { transform: scale(0.95); }
|
|
281
|
+
100% { transform: scale(1); }
|
|
282
|
+
}
|
|
283
|
+
|
|
246
284
|
/* AI avatar mood animations */
|
|
247
285
|
@keyframes ai-pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.15); } }
|
|
248
286
|
@keyframes ai-glow { 0%,100% { box-shadow: 0 0 0 0 rgba(99,102,241,0); } 50% { box-shadow: 0 0 12px 4px rgba(99,102,241,0.35); } }
|
|
@@ -414,6 +452,16 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
|
|
|
414
452
|
font-family: var(--font-sora), 'Sora', system-ui, sans-serif;
|
|
415
453
|
}
|
|
416
454
|
|
|
455
|
+
/* Monospace micro-label utility */
|
|
456
|
+
.label-mono {
|
|
457
|
+
font-family: var(--font-mono);
|
|
458
|
+
font-size: 10px;
|
|
459
|
+
font-weight: 600;
|
|
460
|
+
letter-spacing: 0.06em;
|
|
461
|
+
text-transform: uppercase;
|
|
462
|
+
color: var(--color-text-3);
|
|
463
|
+
}
|
|
464
|
+
|
|
417
465
|
/* ===== Status Badge Classes ===== */
|
|
418
466
|
.status-badge-idle, .status-badge-running, .status-badge-error,
|
|
419
467
|
.status-badge-connecting, .status-badge-connected, .status-badge-disconnected,
|