@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,284 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { genId } from '@/lib/id'
|
|
3
|
+
import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
6
|
+
import { streamAgentChat } from '@/lib/server/stream-agent-chat'
|
|
7
|
+
import { getProvider } from '@/lib/providers'
|
|
8
|
+
import {
|
|
9
|
+
resolveApiKey,
|
|
10
|
+
parseMentions,
|
|
11
|
+
buildChatroomSystemPrompt,
|
|
12
|
+
buildSyntheticSession,
|
|
13
|
+
buildAgentSystemPromptForChatroom,
|
|
14
|
+
buildHistoryForAgent,
|
|
15
|
+
} from '@/lib/server/chatroom-helpers'
|
|
16
|
+
import type { Chatroom, ChatroomMessage, Agent } from '@/types'
|
|
17
|
+
|
|
18
|
+
export const dynamic = 'force-dynamic'
|
|
19
|
+
export const maxDuration = 300
|
|
20
|
+
|
|
21
|
+
const MAX_CHAIN_DEPTH = 5
|
|
22
|
+
|
|
23
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
24
|
+
const { id } = await params
|
|
25
|
+
const body = await req.json()
|
|
26
|
+
|
|
27
|
+
const chatrooms = loadChatrooms()
|
|
28
|
+
const chatroom = chatrooms[id] as Chatroom | undefined
|
|
29
|
+
if (!chatroom) return notFound()
|
|
30
|
+
|
|
31
|
+
const text = typeof body.text === 'string' ? body.text : ''
|
|
32
|
+
const senderId = typeof body.senderId === 'string' ? body.senderId : 'user'
|
|
33
|
+
const imagePath = typeof body.imagePath === 'string' ? body.imagePath : undefined
|
|
34
|
+
const attachedFiles = Array.isArray(body.attachedFiles)
|
|
35
|
+
? (body.attachedFiles as unknown[]).filter((f): f is string => typeof f === 'string')
|
|
36
|
+
: undefined
|
|
37
|
+
const replyToId = typeof body.replyToId === 'string' ? body.replyToId : undefined
|
|
38
|
+
|
|
39
|
+
if (!text.trim() && !imagePath && !attachedFiles?.length) {
|
|
40
|
+
return NextResponse.json({ error: 'text or attachment is required' }, { status: 400 })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const agents = loadAgents() as Record<string, Agent>
|
|
44
|
+
|
|
45
|
+
// Persist incoming message
|
|
46
|
+
const senderName = senderId === 'user' ? 'You' : (agents[senderId]?.name || senderId)
|
|
47
|
+
let mentions = parseMentions(text, agents, chatroom.agentIds)
|
|
48
|
+
// Auto-address: if enabled and no explicit mentions, address all agents
|
|
49
|
+
if (chatroom.autoAddress && mentions.length === 0) {
|
|
50
|
+
mentions = [...chatroom.agentIds]
|
|
51
|
+
}
|
|
52
|
+
const userMessage: ChatroomMessage = {
|
|
53
|
+
id: genId(),
|
|
54
|
+
senderId,
|
|
55
|
+
senderName,
|
|
56
|
+
role: senderId === 'user' ? 'user' : 'assistant',
|
|
57
|
+
text,
|
|
58
|
+
mentions,
|
|
59
|
+
reactions: [],
|
|
60
|
+
time: Date.now(),
|
|
61
|
+
...(imagePath ? { imagePath } : {}),
|
|
62
|
+
...(attachedFiles ? { attachedFiles } : {}),
|
|
63
|
+
...(replyToId ? { replyToId } : {}),
|
|
64
|
+
}
|
|
65
|
+
chatroom.messages.push(userMessage)
|
|
66
|
+
chatroom.updatedAt = Date.now()
|
|
67
|
+
chatrooms[id] = chatroom
|
|
68
|
+
saveChatrooms(chatrooms)
|
|
69
|
+
notify('chatrooms')
|
|
70
|
+
notify(`chatroom:${id}`)
|
|
71
|
+
|
|
72
|
+
// Build reply context if replying to a message
|
|
73
|
+
let replyContext = ''
|
|
74
|
+
if (replyToId) {
|
|
75
|
+
const replyMsg = chatroom.messages.find((m) => m.id === replyToId)
|
|
76
|
+
if (replyMsg) {
|
|
77
|
+
const truncated = replyMsg.text.length > 200 ? replyMsg.text.slice(0, 200) + '...' : replyMsg.text
|
|
78
|
+
replyContext = `> [${replyMsg.senderName}]: ${truncated}\n\n`
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// SSE stream
|
|
83
|
+
const encoder = new TextEncoder()
|
|
84
|
+
const stream = new ReadableStream({
|
|
85
|
+
start(controller) {
|
|
86
|
+
let closed = false
|
|
87
|
+
const writeEvent = (event: Record<string, unknown>) => {
|
|
88
|
+
if (closed) return
|
|
89
|
+
try {
|
|
90
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`))
|
|
91
|
+
} catch {
|
|
92
|
+
closed = true
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const processAgents = async () => {
|
|
97
|
+
// Build agent queue: start with mentioned agents, then chain
|
|
98
|
+
const initialQueue: Array<{ agentId: string; depth: number; contextMessage?: string }> = mentions.map((aid) => ({ agentId: aid, depth: 0 }))
|
|
99
|
+
const processed = new Set<string>()
|
|
100
|
+
const agentQueue: Array<{ agentId: string; depth: number; contextMessage?: string }> = []
|
|
101
|
+
|
|
102
|
+
/** Process a single agent: stream response, persist message, return chained mentions */
|
|
103
|
+
const processOneAgent = async (item: { agentId: string; depth: number; contextMessage?: string }): Promise<string[]> => {
|
|
104
|
+
if (processed.has(item.agentId) || item.depth >= MAX_CHAIN_DEPTH) return []
|
|
105
|
+
processed.add(item.agentId)
|
|
106
|
+
|
|
107
|
+
const agent = agents[item.agentId]
|
|
108
|
+
if (!agent) return []
|
|
109
|
+
|
|
110
|
+
// Pre-flight: check if the agent's provider is usable before attempting to stream
|
|
111
|
+
const providerInfo = getProvider(agent.provider)
|
|
112
|
+
const apiKey = resolveApiKey(agent.credentialId)
|
|
113
|
+
if (providerInfo?.requiresApiKey && !apiKey) {
|
|
114
|
+
writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
|
|
115
|
+
writeEvent({ t: 'err', text: `${agent.name} has no API credentials configured`, agentId: agent.id, agentName: agent.name })
|
|
116
|
+
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
117
|
+
return []
|
|
118
|
+
}
|
|
119
|
+
if (providerInfo?.requiresEndpoint && !agent.apiEndpoint) {
|
|
120
|
+
writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
|
|
121
|
+
writeEvent({ t: 'err', text: `${agent.name} has no endpoint configured`, agentId: agent.id, agentName: agent.name })
|
|
122
|
+
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
123
|
+
return []
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const freshChatrooms = loadChatrooms()
|
|
130
|
+
const freshChatroom = freshChatrooms[id] as Chatroom
|
|
131
|
+
|
|
132
|
+
const syntheticSession = buildSyntheticSession(agent, id)
|
|
133
|
+
const agentSystemPrompt = buildAgentSystemPromptForChatroom(agent)
|
|
134
|
+
const chatroomContext = buildChatroomSystemPrompt(freshChatroom, agents, agent.id)
|
|
135
|
+
const fullSystemPrompt = [agentSystemPrompt, chatroomContext].filter(Boolean).join('\n\n')
|
|
136
|
+
const history = buildHistoryForAgent(freshChatroom, agent.id, imagePath, attachedFiles)
|
|
137
|
+
|
|
138
|
+
// Use enriched context message for chained agents, or reply context + original text
|
|
139
|
+
const messageForAgent = item.contextMessage || (replyContext + text)
|
|
140
|
+
|
|
141
|
+
let fullText = ''
|
|
142
|
+
let agentError = ''
|
|
143
|
+
const result = await streamAgentChat({
|
|
144
|
+
session: syntheticSession,
|
|
145
|
+
message: messageForAgent,
|
|
146
|
+
imagePath,
|
|
147
|
+
attachedFiles,
|
|
148
|
+
apiKey,
|
|
149
|
+
systemPrompt: fullSystemPrompt,
|
|
150
|
+
write: (raw: string) => {
|
|
151
|
+
const lines = raw.split('\n').filter(Boolean)
|
|
152
|
+
for (const line of lines) {
|
|
153
|
+
if (!line.startsWith('data: ')) continue
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(line.slice(6).trim())
|
|
156
|
+
if (parsed.t === 'd' && parsed.text) {
|
|
157
|
+
fullText += parsed.text
|
|
158
|
+
writeEvent({ t: 'd', text: parsed.text, agentId: agent.id, agentName: agent.name })
|
|
159
|
+
} else if (parsed.t === 'tool_call' || parsed.t === 'tool_result') {
|
|
160
|
+
writeEvent({ ...parsed, agentId: agent.id, agentName: agent.name })
|
|
161
|
+
} else if (parsed.t === 'err' && parsed.text) {
|
|
162
|
+
agentError = parsed.text
|
|
163
|
+
writeEvent({ t: 'err', text: parsed.text, agentId: agent.id, agentName: agent.name })
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// skip malformed lines
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
history,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const responseText = result.fullText || fullText
|
|
174
|
+
|
|
175
|
+
// Don't persist empty or error-only messages — they pollute chat history
|
|
176
|
+
if (!responseText.trim() && agentError) {
|
|
177
|
+
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
178
|
+
return []
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (responseText.trim()) {
|
|
182
|
+
const newMentions = parseMentions(responseText, agents, freshChatroom.agentIds)
|
|
183
|
+
const agentMessage: ChatroomMessage = {
|
|
184
|
+
id: genId(),
|
|
185
|
+
senderId: agent.id,
|
|
186
|
+
senderName: agent.name,
|
|
187
|
+
role: 'assistant',
|
|
188
|
+
text: responseText,
|
|
189
|
+
mentions: newMentions,
|
|
190
|
+
reactions: [],
|
|
191
|
+
time: Date.now(),
|
|
192
|
+
}
|
|
193
|
+
const latestChatrooms = loadChatrooms()
|
|
194
|
+
const latestChatroom = latestChatrooms[id] as Chatroom
|
|
195
|
+
latestChatroom.messages.push(agentMessage)
|
|
196
|
+
latestChatroom.updatedAt = Date.now()
|
|
197
|
+
latestChatrooms[id] = latestChatroom
|
|
198
|
+
saveChatrooms(latestChatrooms)
|
|
199
|
+
notify(`chatroom:${id}`)
|
|
200
|
+
|
|
201
|
+
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
202
|
+
|
|
203
|
+
// Return chained agent IDs — enriched context is built below when queuing
|
|
204
|
+
return newMentions.filter((mid) => !processed.has(mid) && freshChatroom.agentIds.includes(mid))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
208
|
+
return []
|
|
209
|
+
} catch (err: unknown) {
|
|
210
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
211
|
+
writeEvent({ t: 'err', text: `Agent ${agent.name} error: ${msg}`, agentId: agent.id })
|
|
212
|
+
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
213
|
+
return []
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (chatroom.chatMode === 'parallel') {
|
|
218
|
+
// Process initial batch in parallel
|
|
219
|
+
const results = await Promise.all(initialQueue.map(processOneAgent))
|
|
220
|
+
// Chained agents from parallel responses queue sequentially
|
|
221
|
+
for (const chainedIds of results) {
|
|
222
|
+
for (const cid of chainedIds) {
|
|
223
|
+
agentQueue.push({ agentId: cid, depth: 1 })
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
// Sequential: push initial queue items
|
|
228
|
+
agentQueue.push(...initialQueue)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Process remaining chained agents sequentially with enriched context
|
|
232
|
+
while (agentQueue.length > 0) {
|
|
233
|
+
const item = agentQueue.shift()!
|
|
234
|
+
|
|
235
|
+
// Build enriched context for chained agents by looking at the most recent message
|
|
236
|
+
if (item.depth > 0 && !item.contextMessage) {
|
|
237
|
+
const latestChatrooms = loadChatrooms()
|
|
238
|
+
const latestChatroom = latestChatrooms[id] as Chatroom
|
|
239
|
+
const lastAgentMsg = [...latestChatroom.messages].reverse().find(
|
|
240
|
+
(m) => m.role === 'assistant' && m.senderId !== item.agentId
|
|
241
|
+
)
|
|
242
|
+
if (lastAgentMsg) {
|
|
243
|
+
const truncated = lastAgentMsg.text.length > 500 ? lastAgentMsg.text.slice(0, 500) + '...' : lastAgentMsg.text
|
|
244
|
+
item.contextMessage = `${lastAgentMsg.senderName} said: "${truncated}" — They're requesting your help. Review the conversation and respond.`
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const chainedIds = await processOneAgent(item)
|
|
249
|
+
for (const cid of chainedIds) {
|
|
250
|
+
agentQueue.push({ agentId: cid, depth: item.depth + 1 })
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
writeEvent({ t: 'done' })
|
|
255
|
+
if (!closed) {
|
|
256
|
+
try { controller.close() } catch { /* already closed */ }
|
|
257
|
+
closed = true
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
processAgents().catch((err) => {
|
|
262
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
263
|
+
writeEvent({ t: 'err', text: msg })
|
|
264
|
+
writeEvent({ t: 'done' })
|
|
265
|
+
if (!closed) {
|
|
266
|
+
try { controller.close() } catch { /* already closed */ }
|
|
267
|
+
closed = true
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
},
|
|
271
|
+
cancel() {
|
|
272
|
+
// Client disconnected
|
|
273
|
+
},
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
return new NextResponse(stream, {
|
|
277
|
+
headers: {
|
|
278
|
+
'Content-Type': 'text/event-stream',
|
|
279
|
+
'Cache-Control': 'no-cache',
|
|
280
|
+
'Connection': 'keep-alive',
|
|
281
|
+
'X-Accel-Buffering': 'no',
|
|
282
|
+
},
|
|
283
|
+
})
|
|
284
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
|
|
3
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
4
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
import { genId } from '@/lib/id'
|
|
6
|
+
|
|
7
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
|
+
const { id } = await params
|
|
9
|
+
const body = await req.json()
|
|
10
|
+
const chatrooms = loadChatrooms()
|
|
11
|
+
const chatroom = chatrooms[id]
|
|
12
|
+
if (!chatroom) return notFound()
|
|
13
|
+
|
|
14
|
+
const agentId = body.agentId as string
|
|
15
|
+
if (!agentId) return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
|
|
16
|
+
|
|
17
|
+
if (!chatroom.agentIds.includes(agentId)) {
|
|
18
|
+
chatroom.agentIds.push(agentId)
|
|
19
|
+
|
|
20
|
+
// Inject a system event message
|
|
21
|
+
const agents = loadAgents()
|
|
22
|
+
const agentName = agents[agentId]?.name || 'Unknown agent'
|
|
23
|
+
if (!Array.isArray(chatroom.messages)) chatroom.messages = []
|
|
24
|
+
chatroom.messages.push({
|
|
25
|
+
id: genId(),
|
|
26
|
+
senderId: 'system',
|
|
27
|
+
senderName: 'System',
|
|
28
|
+
role: 'assistant',
|
|
29
|
+
text: `${agentName} has joined the chat`,
|
|
30
|
+
mentions: [],
|
|
31
|
+
reactions: [],
|
|
32
|
+
time: Date.now(),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
chatroom.updatedAt = Date.now()
|
|
36
|
+
chatrooms[id] = chatroom
|
|
37
|
+
saveChatrooms(chatrooms)
|
|
38
|
+
notify('chatrooms')
|
|
39
|
+
notify(`chatroom:${id}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return NextResponse.json(chatroom)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
46
|
+
const { id } = await params
|
|
47
|
+
const body = await req.json()
|
|
48
|
+
const chatrooms = loadChatrooms()
|
|
49
|
+
const chatroom = chatrooms[id]
|
|
50
|
+
if (!chatroom) return notFound()
|
|
51
|
+
|
|
52
|
+
const agentId = body.agentId as string
|
|
53
|
+
if (!agentId) return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
|
|
54
|
+
|
|
55
|
+
const wasPresent = chatroom.agentIds.includes(agentId)
|
|
56
|
+
chatroom.agentIds = chatroom.agentIds.filter((aid: string) => aid !== agentId)
|
|
57
|
+
|
|
58
|
+
// Inject a system event message
|
|
59
|
+
if (wasPresent) {
|
|
60
|
+
const agents = loadAgents()
|
|
61
|
+
const agentName = agents[agentId]?.name || 'Unknown agent'
|
|
62
|
+
if (!Array.isArray(chatroom.messages)) chatroom.messages = []
|
|
63
|
+
chatroom.messages.push({
|
|
64
|
+
id: genId(),
|
|
65
|
+
senderId: 'system',
|
|
66
|
+
senderName: 'System',
|
|
67
|
+
role: 'assistant',
|
|
68
|
+
text: `${agentName} has left the chat`,
|
|
69
|
+
mentions: [],
|
|
70
|
+
reactions: [],
|
|
71
|
+
time: Date.now(),
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
chatroom.updatedAt = Date.now()
|
|
76
|
+
chatrooms[id] = chatroom
|
|
77
|
+
saveChatrooms(chatrooms)
|
|
78
|
+
notify('chatrooms')
|
|
79
|
+
notify(`chatroom:${id}`)
|
|
80
|
+
|
|
81
|
+
return NextResponse.json(chatroom)
|
|
82
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadChatrooms, saveChatrooms } from '@/lib/server/storage'
|
|
3
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
4
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
import type { Chatroom } from '@/types'
|
|
6
|
+
|
|
7
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
|
+
const { id } = await params
|
|
9
|
+
const body = await req.json()
|
|
10
|
+
const chatrooms = loadChatrooms()
|
|
11
|
+
const chatroom = chatrooms[id] as Chatroom | undefined
|
|
12
|
+
if (!chatroom) return notFound()
|
|
13
|
+
|
|
14
|
+
const messageId = body.messageId as string
|
|
15
|
+
if (!messageId) {
|
|
16
|
+
return NextResponse.json({ error: 'messageId is required' }, { status: 400 })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const message = chatroom.messages.find((m) => m.id === messageId)
|
|
20
|
+
if (!message) {
|
|
21
|
+
return NextResponse.json({ error: 'Message not found' }, { status: 404 })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Toggle: remove if pinned, add if not
|
|
25
|
+
if (!chatroom.pinnedMessageIds) chatroom.pinnedMessageIds = []
|
|
26
|
+
const idx = chatroom.pinnedMessageIds.indexOf(messageId)
|
|
27
|
+
if (idx >= 0) {
|
|
28
|
+
chatroom.pinnedMessageIds.splice(idx, 1)
|
|
29
|
+
} else {
|
|
30
|
+
chatroom.pinnedMessageIds.push(messageId)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
chatroom.updatedAt = Date.now()
|
|
34
|
+
chatrooms[id] = chatroom
|
|
35
|
+
saveChatrooms(chatrooms)
|
|
36
|
+
notify(`chatroom:${id}`)
|
|
37
|
+
|
|
38
|
+
return NextResponse.json({ ok: true, pinnedMessageIds: chatroom.pinnedMessageIds })
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadChatrooms, saveChatrooms } from '@/lib/server/storage'
|
|
3
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
4
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
import type { Chatroom, ChatroomMessage, ChatroomReaction } from '@/types'
|
|
6
|
+
|
|
7
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
|
+
const { id } = await params
|
|
9
|
+
const body = await req.json()
|
|
10
|
+
const chatrooms = loadChatrooms()
|
|
11
|
+
const chatroom = chatrooms[id] as Chatroom | undefined
|
|
12
|
+
if (!chatroom) return notFound()
|
|
13
|
+
|
|
14
|
+
const messageId = body.messageId as string
|
|
15
|
+
const emoji = body.emoji as string
|
|
16
|
+
const reactorId = (body.reactorId as string) || 'user'
|
|
17
|
+
if (!messageId || !emoji) {
|
|
18
|
+
return NextResponse.json({ error: 'messageId and emoji are required' }, { status: 400 })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const message = chatroom.messages.find((m: ChatroomMessage) => m.id === messageId)
|
|
22
|
+
if (!message) {
|
|
23
|
+
return NextResponse.json({ error: 'Message not found' }, { status: 404 })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Toggle: remove if already exists, add if not
|
|
27
|
+
const existingIdx = message.reactions.findIndex(
|
|
28
|
+
(r: ChatroomReaction) => r.emoji === emoji && r.reactorId === reactorId
|
|
29
|
+
)
|
|
30
|
+
if (existingIdx >= 0) {
|
|
31
|
+
message.reactions.splice(existingIdx, 1)
|
|
32
|
+
} else {
|
|
33
|
+
message.reactions.push({ emoji, reactorId, time: Date.now() })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
chatroom.updatedAt = Date.now()
|
|
37
|
+
chatrooms[id] = chatroom
|
|
38
|
+
saveChatrooms(chatrooms)
|
|
39
|
+
notify(`chatroom:${id}`)
|
|
40
|
+
|
|
41
|
+
return NextResponse.json({ ok: true, reactions: message.reactions })
|
|
42
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
|
|
3
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
4
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
import { genId } from '@/lib/id'
|
|
6
|
+
|
|
7
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
|
+
const { id } = await params
|
|
9
|
+
const chatrooms = loadChatrooms()
|
|
10
|
+
const chatroom = chatrooms[id]
|
|
11
|
+
if (!chatroom) return notFound()
|
|
12
|
+
return NextResponse.json(chatroom)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
16
|
+
const { id } = await params
|
|
17
|
+
const body = await req.json()
|
|
18
|
+
const chatrooms = loadChatrooms()
|
|
19
|
+
const chatroom = chatrooms[id]
|
|
20
|
+
if (!chatroom) return notFound()
|
|
21
|
+
|
|
22
|
+
if (body.name !== undefined) chatroom.name = body.name
|
|
23
|
+
if (body.description !== undefined) chatroom.description = body.description
|
|
24
|
+
|
|
25
|
+
// Diff agentIds and inject join/leave system messages
|
|
26
|
+
if (Array.isArray(body.agentIds)) {
|
|
27
|
+
const oldIds = new Set(chatroom.agentIds)
|
|
28
|
+
const newIds = new Set(body.agentIds as string[])
|
|
29
|
+
const added = (body.agentIds as string[]).filter((aid: string) => !oldIds.has(aid))
|
|
30
|
+
const removed = chatroom.agentIds.filter((aid: string) => !newIds.has(aid))
|
|
31
|
+
|
|
32
|
+
if (added.length > 0 || removed.length > 0) {
|
|
33
|
+
const agents = loadAgents()
|
|
34
|
+
if (!Array.isArray(chatroom.messages)) chatroom.messages = []
|
|
35
|
+
const now = Date.now()
|
|
36
|
+
let offset = 0
|
|
37
|
+
for (const aid of added) {
|
|
38
|
+
chatroom.messages.push({
|
|
39
|
+
id: genId(),
|
|
40
|
+
senderId: 'system',
|
|
41
|
+
senderName: 'System',
|
|
42
|
+
role: 'assistant',
|
|
43
|
+
text: `${agents[aid]?.name || 'Unknown agent'} has joined the chat`,
|
|
44
|
+
mentions: [],
|
|
45
|
+
reactions: [],
|
|
46
|
+
time: now + offset++,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
for (const aid of removed) {
|
|
50
|
+
chatroom.messages.push({
|
|
51
|
+
id: genId(),
|
|
52
|
+
senderId: 'system',
|
|
53
|
+
senderName: 'System',
|
|
54
|
+
role: 'assistant',
|
|
55
|
+
text: `${agents[aid]?.name || 'Unknown agent'} has left the chat`,
|
|
56
|
+
mentions: [],
|
|
57
|
+
reactions: [],
|
|
58
|
+
time: now + offset++,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
chatroom.agentIds = body.agentIds
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
chatroom.updatedAt = Date.now()
|
|
67
|
+
|
|
68
|
+
chatrooms[id] = chatroom
|
|
69
|
+
saveChatrooms(chatrooms)
|
|
70
|
+
notify('chatrooms')
|
|
71
|
+
notify(`chatroom:${id}`)
|
|
72
|
+
return NextResponse.json(chatroom)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
76
|
+
const { id } = await params
|
|
77
|
+
const chatrooms = loadChatrooms()
|
|
78
|
+
if (!chatrooms[id]) return notFound()
|
|
79
|
+
|
|
80
|
+
delete chatrooms[id]
|
|
81
|
+
saveChatrooms(chatrooms)
|
|
82
|
+
notify('chatrooms')
|
|
83
|
+
return NextResponse.json({ ok: true })
|
|
84
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { genId } from '@/lib/id'
|
|
3
|
+
import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
import type { Chatroom, ChatroomMessage } from '@/types'
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic'
|
|
8
|
+
|
|
9
|
+
export async function GET() {
|
|
10
|
+
const chatrooms = loadChatrooms()
|
|
11
|
+
return NextResponse.json(chatrooms)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function POST(req: Request) {
|
|
15
|
+
const body = await req.json()
|
|
16
|
+
const chatrooms = loadChatrooms()
|
|
17
|
+
const id = genId()
|
|
18
|
+
|
|
19
|
+
const agentIds: string[] = Array.isArray(body.agentIds) ? body.agentIds : []
|
|
20
|
+
const now = Date.now()
|
|
21
|
+
|
|
22
|
+
// Generate join messages for initial agents
|
|
23
|
+
const agents = agentIds.length > 0 ? loadAgents() : {}
|
|
24
|
+
const joinMessages: ChatroomMessage[] = agentIds.map((agentId: string, i: number) => ({
|
|
25
|
+
id: genId(),
|
|
26
|
+
senderId: 'system',
|
|
27
|
+
senderName: 'System',
|
|
28
|
+
role: 'assistant',
|
|
29
|
+
text: `${agents[agentId]?.name || 'Unknown agent'} has joined the chat`,
|
|
30
|
+
mentions: [],
|
|
31
|
+
reactions: [],
|
|
32
|
+
time: now + i, // offset by 1ms so they sort in order
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
const chatroom: Chatroom = {
|
|
36
|
+
id,
|
|
37
|
+
name: body.name || 'New Chatroom',
|
|
38
|
+
description: body.description || '',
|
|
39
|
+
agentIds,
|
|
40
|
+
messages: joinMessages,
|
|
41
|
+
createdAt: now,
|
|
42
|
+
updatedAt: now,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
chatrooms[id] = chatroom
|
|
46
|
+
saveChatrooms(chatrooms)
|
|
47
|
+
notify('chatrooms')
|
|
48
|
+
|
|
49
|
+
return NextResponse.json(chatroom)
|
|
50
|
+
}
|
|
@@ -61,6 +61,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
61
61
|
// Regular update
|
|
62
62
|
if (body.name !== undefined) connector.name = body.name
|
|
63
63
|
if (body.agentId !== undefined) connector.agentId = body.agentId
|
|
64
|
+
if (body.chatroomId !== undefined) connector.chatroomId = body.chatroomId
|
|
64
65
|
if (body.credentialId !== undefined) connector.credentialId = body.credentialId
|
|
65
66
|
if (body.config !== undefined) connector.config = body.config
|
|
66
67
|
if (body.isEnabled !== undefined) connector.isEnabled = body.isEnabled
|
|
@@ -33,7 +33,8 @@ export async function POST(req: Request) {
|
|
|
33
33
|
id,
|
|
34
34
|
name: body.name || `${body.platform} Connector`,
|
|
35
35
|
platform: body.platform,
|
|
36
|
-
agentId: body.agentId,
|
|
36
|
+
agentId: body.agentId || null,
|
|
37
|
+
chatroomId: body.chatroomId || null,
|
|
37
38
|
credentialId: body.credentialId || null,
|
|
38
39
|
config: body.config || {},
|
|
39
40
|
isEnabled: false,
|
|
@@ -6,8 +6,8 @@ export const dynamic = 'force-dynamic'
|
|
|
6
6
|
|
|
7
7
|
export async function GET(_req: Request) {
|
|
8
8
|
const creds = loadCredentials()
|
|
9
|
-
const safe: Record<string,
|
|
10
|
-
for (const [id, c] of Object.entries(creds) as [string,
|
|
9
|
+
const safe: Record<string, Record<string, unknown>> = {}
|
|
10
|
+
for (const [id, c] of Object.entries(creds) as [string, Record<string, unknown>][]) {
|
|
11
11
|
safe[id] = { id: c.id, provider: c.provider, name: c.name, createdAt: c.createdAt }
|
|
12
12
|
}
|
|
13
13
|
return NextResponse.json(safe)
|
|
@@ -28,6 +28,5 @@ export async function POST(req: Request) {
|
|
|
28
28
|
createdAt: Date.now(),
|
|
29
29
|
}
|
|
30
30
|
saveCredentials(creds)
|
|
31
|
-
console.log(`[credentials] stored ${id} for ${provider}`)
|
|
32
31
|
return NextResponse.json({ id, provider, name: creds[id].name, createdAt: creds[id].createdAt })
|
|
33
32
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { exec } from 'child_process'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
|
|
6
|
+
export async function POST(req: Request) {
|
|
7
|
+
const { path: targetPath } = await req.json() as { path?: string }
|
|
8
|
+
if (!targetPath || typeof targetPath !== 'string') {
|
|
9
|
+
return NextResponse.json({ error: 'path is required' }, { status: 400 })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const resolved = path.resolve(targetPath)
|
|
13
|
+
|
|
14
|
+
// Verify the path exists
|
|
15
|
+
if (!fs.existsSync(resolved)) {
|
|
16
|
+
return NextResponse.json({ error: 'Path does not exist' }, { status: 404 })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isDir = fs.statSync(resolved).isDirectory()
|
|
20
|
+
const platform = process.platform
|
|
21
|
+
|
|
22
|
+
// Determine the command to reveal in the OS file manager
|
|
23
|
+
let cmd: string
|
|
24
|
+
if (platform === 'darwin') {
|
|
25
|
+
// macOS: -R reveals in Finder (selects the item), for dirs just open the dir
|
|
26
|
+
cmd = isDir ? `open "${resolved}"` : `open -R "${resolved}"`
|
|
27
|
+
} else if (platform === 'win32') {
|
|
28
|
+
cmd = isDir ? `explorer "${resolved}"` : `explorer /select,"${resolved}"`
|
|
29
|
+
} else {
|
|
30
|
+
// Linux: xdg-open on the directory containing the file
|
|
31
|
+
cmd = `xdg-open "${isDir ? resolved : path.dirname(resolved)}"`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return new Promise<NextResponse>((resolve) => {
|
|
35
|
+
exec(cmd, (err) => {
|
|
36
|
+
if (err) {
|
|
37
|
+
resolve(NextResponse.json({ error: err.message }, { status: 500 }))
|
|
38
|
+
} else {
|
|
39
|
+
resolve(NextResponse.json({ ok: true }))
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
}
|