@swarmclawai/swarmclaw 0.6.0 → 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 +15 -2
- package/bin/server-cmd.js +1 -0
- package/package.json +2 -1
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/search/route.ts +9 -7
- 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/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +17 -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/globals.css +5 -0
- package/src/cli/index.js +16 -1
- package/src/cli/spec.js +26 -0
- package/src/components/agents/agent-card.tsx +3 -3
- package/src/components/agents/agent-chat-list.tsx +29 -6
- package/src/components/agents/agent-sheet.tsx +66 -4
- 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/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +8 -4
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +455 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +23 -2
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/message-bubble.tsx +315 -25
- package/src/components/chat/message-list.tsx +180 -7
- package/src/components/chat/streaming-bubble.tsx +68 -1
- package/src/components/chat/tool-call-bubble.tsx +45 -3
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/chatroom-list.tsx +8 -1
- package/src/components/chatrooms/chatroom-message.tsx +8 -3
- package/src/components/chatrooms/chatroom-view.tsx +3 -3
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +68 -16
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/input/chat-input.tsx +28 -2
- package/src/components/layout/app-layout.tsx +19 -2
- package/src/components/projects/project-detail.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +260 -127
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/shared/search-dialog.tsx +17 -10
- 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-storage.tsx +206 -0
- package/src/components/shared/settings/section-user-preferences.tsx +18 -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 +3 -1
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/tasks/task-card.tsx +14 -1
- package/src/components/tasks/task-sheet.tsx +328 -3
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- 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 +51 -11
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +218 -7
- package/src/lib/server/heartbeat-service.ts +8 -1
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +15 -2
- package/src/lib/server/memory-db.ts +134 -6
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +2 -2
- 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/queue.ts +52 -7
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- 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 +8 -0
- package/src/lib/server/session-tools/memory.ts +1 -0
- 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/stream-agent-chat.ts +32 -10
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/stores/use-app-store.ts +5 -1
- package/src/stores/use-chat-store.ts +65 -2
- package/src/types/index.ts +32 -2
package/README.md
CHANGED
|
@@ -30,11 +30,13 @@ Inspired by [OpenClaw](https://github.com/openclaw).
|
|
|
30
30
|
- **Agent Builder** — Create agents with custom personalities (soul), system prompts, tools, and skills. AI-powered generation from a description
|
|
31
31
|
- **Agent Inspector Panel** — Per-agent side panel for OpenClaw file editing (`SOUL.md`, `IDENTITY.md`, `USER.md`, etc.), guided personality editing, skill install/enable/remove, permission presets, sandbox env allowlist, and cron automations
|
|
32
32
|
- **Agent Fleet Management** — Avatar seeds with generated avatars, running/approval fleet filters, soft-delete agent trash with restore/permanent delete, and approval counters in agent cards
|
|
33
|
-
- **Agent Tools** — Shell, process control for long-running commands, files, edit file, send file, web search, web fetch, CLI delegation (Claude/Codex/OpenCode), Playwright browser automation, persistent memory, and sandboxed code execution (JS/TS via Deno, Python)
|
|
33
|
+
- **Agent Tools** — Shell, process control for long-running commands, files, edit file, send file, web search, web fetch, CLI delegation (Claude/Codex/OpenCode), Playwright browser automation, sub-agent spawning, canvas presentation, direct HTTP requests, git operations, persistent memory, and sandboxed code execution (JS/TS via Deno, Python)
|
|
34
34
|
- **Platform Tools** — Agents can manage other agents, tasks, schedules, skills, connectors, sessions, and encrypted secrets via built-in platform tools
|
|
35
35
|
- **Orchestration** — Multi-agent workflows powered by LangGraph with automatic sub-agent routing, checkpointed execution, and rich delegation cards that link to sub-agent chat threads
|
|
36
36
|
- **Agentic Execution Policy** — Tool-first autonomous action loop with progress updates, evidence-driven answers, and better use of platform tools for long-lived work
|
|
37
|
+
- **Runtime Date/Time Grounding** — Session, orchestrator, chatroom, and connector prompts include authoritative current timestamp context to reduce stale-date behavior
|
|
37
38
|
- **Task Board** — Queue and track agent tasks with status, comments, results, and archiving. Strict capability policy pauses tasks for human approval before tool execution
|
|
39
|
+
- **Task Metrics API** — Built-in analytics endpoint for WIP, cycle times, throughput velocity, completion/failure by agent, and priority distribution
|
|
38
40
|
- **Background Daemon** — Auto-processes queued tasks and scheduled jobs with a 30s heartbeat plus recurring health monitoring
|
|
39
41
|
- **Scheduling** — Cron-based agent scheduling with human-friendly presets
|
|
40
42
|
- **Loop Runtime Controls** — Switch between bounded and ongoing loops with configurable step caps, runtime guards, heartbeat cadence, and timeout budgets
|
|
@@ -86,7 +88,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
|
|
|
86
88
|
```
|
|
87
89
|
|
|
88
90
|
The installer resolves the latest stable release tag and installs that version by default.
|
|
89
|
-
To pin a version: `SWARMCLAW_VERSION=v0.6.
|
|
91
|
+
To pin a version: `SWARMCLAW_VERSION=v0.6.1 curl ... | bash`
|
|
90
92
|
|
|
91
93
|
Or run locally from the repo (friendly for non-technical users):
|
|
92
94
|
|
|
@@ -279,7 +281,11 @@ Agents can use the following tools when enabled:
|
|
|
279
281
|
| Web Search | Search the web via DuckDuckGo HTML scraping |
|
|
280
282
|
| Web Fetch | Fetch and extract text content from URLs (uses cheerio) |
|
|
281
283
|
| CLI Delegation | Delegate complex tasks to Claude Code, Codex CLI, or OpenCode CLI |
|
|
284
|
+
| Spawn Subagent | Delegate a sub-task to another agent and capture its response in the current run |
|
|
282
285
|
| Browser | Playwright-powered web browsing via MCP (navigate, click, type, screenshot, PDF) |
|
|
286
|
+
| Canvas | Present/hide/snapshot live HTML content in a session canvas panel |
|
|
287
|
+
| HTTP Request | Make direct API calls with method, headers, body, redirect control, and timeout |
|
|
288
|
+
| Git | Run structured git subcommands (`status`, `diff`, `log`, `add`, `commit`, `push`, etc.) with repo safety checks |
|
|
283
289
|
| Memory | Store and retrieve long-term memories with FTS5 + vector search, file references, image attachments, and linked memory graph traversal |
|
|
284
290
|
| Sandbox | Run JS/TS (Deno) or Python code in an isolated sandbox. Created files are returned as downloadable artifacts |
|
|
285
291
|
| MCP Servers | Connect to external Model Context Protocol servers. Tools from MCP servers are injected as first-class agent tools |
|
|
@@ -319,6 +325,13 @@ Token usage and estimated costs are tracked per message for API-based providers
|
|
|
319
325
|
- **Data:** Stored in `data/swarmclaw.db` (usage table)
|
|
320
326
|
- Cost estimates use published model pricing (updated manually in `src/lib/server/cost.ts`)
|
|
321
327
|
|
|
328
|
+
## Task Metrics
|
|
329
|
+
|
|
330
|
+
Task analytics are available via API for dashboarding and release-readiness checks:
|
|
331
|
+
|
|
332
|
+
- **API endpoint:** `GET /api/tasks/metrics?range=24h|7d|30d`
|
|
333
|
+
- **Returns:** status totals, WIP count, completion velocity buckets, avg/p50/p90 cycle time, completion/failure by agent, and priority counts
|
|
334
|
+
|
|
322
335
|
## Background Daemon
|
|
323
336
|
|
|
324
337
|
The daemon auto-processes queued tasks from the scheduler on a 30-second interval. It also runs recurring health checks that detect stale heartbeat sessions and can send proactive WhatsApp alerts when issues are detected. Toggle the daemon from the sidebar indicator or via API.
|
package/bin/server-cmd.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"radix-ui": "^1.4.3",
|
|
85
85
|
"react": "19.2.3",
|
|
86
86
|
"react-dom": "19.2.3",
|
|
87
|
+
"react-hot-toast": "^2.6.0",
|
|
87
88
|
"react-icons": "^5.5.0",
|
|
88
89
|
"react-markdown": "^10.1.0",
|
|
89
90
|
"recharts": "^3.7.0",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadSessions, saveSessions } from '@/lib/server/storage'
|
|
3
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
4
|
+
|
|
5
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ sessionId: string }> }) {
|
|
6
|
+
const { sessionId } = await params
|
|
7
|
+
const sessions = loadSessions()
|
|
8
|
+
const session = sessions[sessionId]
|
|
9
|
+
if (!session) return NextResponse.json({ error: 'Session not found' }, { status: 404 })
|
|
10
|
+
|
|
11
|
+
return NextResponse.json({
|
|
12
|
+
sessionId,
|
|
13
|
+
content: (session as Record<string, unknown>).canvasContent || null,
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function POST(req: Request, { params }: { params: Promise<{ sessionId: string }> }) {
|
|
18
|
+
const { sessionId } = await params
|
|
19
|
+
const body = await req.json()
|
|
20
|
+
const sessions = loadSessions()
|
|
21
|
+
const session = sessions[sessionId]
|
|
22
|
+
if (!session) return NextResponse.json({ error: 'Session not found' }, { status: 404 })
|
|
23
|
+
|
|
24
|
+
;(session as Record<string, unknown>).canvasContent = body.content || null
|
|
25
|
+
session.lastActiveAt = Date.now()
|
|
26
|
+
sessions[sessionId] = session
|
|
27
|
+
saveSessions(sessions)
|
|
28
|
+
|
|
29
|
+
notify(`canvas:${sessionId}`)
|
|
30
|
+
return NextResponse.json({ ok: true, sessionId })
|
|
31
|
+
}
|
|
@@ -1,151 +1,25 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
|
-
import { loadChatrooms, saveChatrooms, loadAgents
|
|
3
|
+
import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
|
|
4
4
|
import { notify } from '@/lib/server/ws-hub'
|
|
5
5
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
6
6
|
import { streamAgentChat } from '@/lib/server/stream-agent-chat'
|
|
7
7
|
import { getProvider } from '@/lib/providers'
|
|
8
|
-
import
|
|
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'
|
|
9
17
|
|
|
10
18
|
export const dynamic = 'force-dynamic'
|
|
11
19
|
export const maxDuration = 300
|
|
12
20
|
|
|
13
21
|
const MAX_CHAIN_DEPTH = 5
|
|
14
22
|
|
|
15
|
-
/** Resolve API key from an agent's credentialId */
|
|
16
|
-
function resolveApiKey(credentialId: string | null | undefined): string | null {
|
|
17
|
-
if (!credentialId) return null
|
|
18
|
-
const creds = loadCredentials()
|
|
19
|
-
const cred = creds[credentialId]
|
|
20
|
-
if (!cred?.encryptedKey) return null
|
|
21
|
-
try { return decryptKey(cred.encryptedKey) } catch { return null }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Parse @mentions from message text, returns matching agentIds */
|
|
25
|
-
function parseMentions(text: string, agents: Record<string, Agent>, memberIds: string[]): string[] {
|
|
26
|
-
if (/@all\b/i.test(text)) return [...memberIds]
|
|
27
|
-
const mentionPattern = /@(\S+)/g
|
|
28
|
-
const mentioned: string[] = []
|
|
29
|
-
let match: RegExpExecArray | null
|
|
30
|
-
while ((match = mentionPattern.exec(text)) !== null) {
|
|
31
|
-
const name = match[1].toLowerCase()
|
|
32
|
-
for (const id of memberIds) {
|
|
33
|
-
const agent = agents[id]
|
|
34
|
-
if (agent && agent.name.toLowerCase().replace(/\s+/g, '') === name) {
|
|
35
|
-
if (!mentioned.includes(id)) mentioned.push(id)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return mentioned
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Build chatroom context as a system prompt addendum with agent profiles and collaboration guidelines */
|
|
43
|
-
function buildChatroomSystemPrompt(chatroom: Chatroom, agents: Record<string, Agent>, agentId: string): string {
|
|
44
|
-
const selfAgent = agents[agentId]
|
|
45
|
-
const selfName = selfAgent?.name || agentId
|
|
46
|
-
|
|
47
|
-
// Build team profiles with capabilities
|
|
48
|
-
const teamProfiles = chatroom.agentIds
|
|
49
|
-
.filter((id) => id !== agentId)
|
|
50
|
-
.map((id) => {
|
|
51
|
-
const a = agents[id]
|
|
52
|
-
if (!a) return null
|
|
53
|
-
const tools = a.tools?.length ? `Tools: ${a.tools.join(', ')}` : 'No specialized tools'
|
|
54
|
-
const desc = a.description || a.soul || 'No description'
|
|
55
|
-
return `- **${a.name}**: ${desc}\n ${tools}`
|
|
56
|
-
})
|
|
57
|
-
.filter(Boolean)
|
|
58
|
-
.join('\n')
|
|
59
|
-
|
|
60
|
-
const recentMessages = chatroom.messages.slice(-30).map((m) => {
|
|
61
|
-
return `[${m.senderName}]: ${m.text}`
|
|
62
|
-
}).join('\n')
|
|
63
|
-
|
|
64
|
-
return [
|
|
65
|
-
`## Chatroom Context`,
|
|
66
|
-
`You are **${selfName}** in chatroom "${chatroom.name}".`,
|
|
67
|
-
selfAgent?.description ? `Your role: ${selfAgent.description}` : '',
|
|
68
|
-
selfAgent?.tools?.length ? `Your tools: ${selfAgent.tools.join(', ')}` : '',
|
|
69
|
-
'',
|
|
70
|
-
'## Team Members',
|
|
71
|
-
teamProfiles || '(no other agents)',
|
|
72
|
-
'',
|
|
73
|
-
'## Collaboration Guidelines',
|
|
74
|
-
'- Before executing complex tasks, briefly discuss your approach with the team.',
|
|
75
|
-
'- When delegating to another agent, explain what you need, why they are best suited, and what output you expect. Example: "@DataBot I need a summary of recent API errors from the logs — you have the shell tool to grep through them."',
|
|
76
|
-
'- If someone mentions a task you are well-suited for, proactively offer to help.',
|
|
77
|
-
'- Do not just @mention mechanically — explain your reasoning when involving others.',
|
|
78
|
-
'- If you can handle a request entirely yourself, just do it. Only delegate what you cannot do.',
|
|
79
|
-
'',
|
|
80
|
-
'## Recent Messages',
|
|
81
|
-
recentMessages || '(no messages yet)',
|
|
82
|
-
].filter((line) => line !== undefined).join('\n')
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Build a synthetic session object for an agent in a chatroom */
|
|
86
|
-
function buildSyntheticSession(agent: Agent, chatroomId: string): Session {
|
|
87
|
-
return {
|
|
88
|
-
id: `chatroom-${chatroomId}-${agent.id}`,
|
|
89
|
-
name: `Chatroom session for ${agent.name}`,
|
|
90
|
-
cwd: process.cwd(),
|
|
91
|
-
user: 'chatroom',
|
|
92
|
-
provider: agent.provider,
|
|
93
|
-
model: agent.model,
|
|
94
|
-
credentialId: agent.credentialId ?? null,
|
|
95
|
-
fallbackCredentialIds: agent.fallbackCredentialIds,
|
|
96
|
-
apiEndpoint: agent.apiEndpoint ?? null,
|
|
97
|
-
claudeSessionId: null,
|
|
98
|
-
messages: [],
|
|
99
|
-
createdAt: Date.now(),
|
|
100
|
-
lastActiveAt: Date.now(),
|
|
101
|
-
tools: agent.tools || [],
|
|
102
|
-
agentId: agent.id,
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Build agent's system prompt including skills */
|
|
107
|
-
function buildAgentSystemPromptForChatroom(agent: Agent): string {
|
|
108
|
-
const settings = loadSettings()
|
|
109
|
-
const parts: string[] = []
|
|
110
|
-
if (settings.userPrompt) parts.push(settings.userPrompt)
|
|
111
|
-
if (agent.soul) parts.push(agent.soul)
|
|
112
|
-
if (agent.systemPrompt) parts.push(agent.systemPrompt)
|
|
113
|
-
if (agent.skillIds?.length) {
|
|
114
|
-
const allSkills = loadSkills()
|
|
115
|
-
for (const skillId of agent.skillIds) {
|
|
116
|
-
const skill = allSkills[skillId]
|
|
117
|
-
if (skill?.content) parts.push(`## Skill: ${skill.name}\n${skill.content}`)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return parts.join('\n\n')
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Convert chatroom messages to Message history format for LLM */
|
|
124
|
-
function buildHistoryForAgent(chatroom: Chatroom, agentId: string, imagePath?: string, attachedFiles?: string[]): Message[] {
|
|
125
|
-
const history = chatroom.messages.slice(-50).map((m) => {
|
|
126
|
-
let msgText = `[${m.senderName}]: ${m.text}`
|
|
127
|
-
// Include attachment info in history
|
|
128
|
-
if (m.attachedFiles?.length) {
|
|
129
|
-
const names = m.attachedFiles.map((f) => f.split('/').pop()).join(', ')
|
|
130
|
-
msgText += `\n[Attached: ${names}]`
|
|
131
|
-
}
|
|
132
|
-
return {
|
|
133
|
-
role: m.senderId === agentId ? 'assistant' as const : 'user' as const,
|
|
134
|
-
text: msgText,
|
|
135
|
-
time: m.time,
|
|
136
|
-
...(m.imagePath ? { imagePath: m.imagePath } : {}),
|
|
137
|
-
...(m.attachedFiles ? { attachedFiles: m.attachedFiles } : {}),
|
|
138
|
-
}
|
|
139
|
-
})
|
|
140
|
-
// Pass through imagePath/attachedFiles from the current message to the last history entry
|
|
141
|
-
if (history.length > 0 && (imagePath || attachedFiles)) {
|
|
142
|
-
const last = history[history.length - 1]
|
|
143
|
-
if (imagePath && !last.imagePath) last.imagePath = imagePath
|
|
144
|
-
if (attachedFiles && !last.attachedFiles) last.attachedFiles = attachedFiles
|
|
145
|
-
}
|
|
146
|
-
return history
|
|
147
|
-
}
|
|
148
|
-
|
|
149
23
|
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
150
24
|
const { id } = await params
|
|
151
25
|
const body = await req.json()
|
|
@@ -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,
|
|
@@ -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
|
+
}
|
|
@@ -59,22 +59,24 @@ function searchMessages(
|
|
|
59
59
|
const MAX_MSG_RESULTS = 10
|
|
60
60
|
for (const [sessionId, session] of Object.entries(sessions)) {
|
|
61
61
|
if (results.length >= MAX_MSG_RESULTS) break
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
if (!Array.isArray(session.messages) || !session.messages.length) continue
|
|
63
|
+
const messages = session.messages as Array<Record<string, unknown>>
|
|
64
64
|
const agentId = session.agentId as string | undefined
|
|
65
65
|
const agentName = agentId && agents[agentId] ? (agents[agentId].name as string) : undefined
|
|
66
66
|
const sessionName = (session.name as string) || 'Untitled'
|
|
67
67
|
for (let i = 0; i < messages.length; i++) {
|
|
68
68
|
if (results.length >= MAX_MSG_RESULTS) break
|
|
69
69
|
const msg = messages[i]
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const text = typeof msg?.text === 'string' ? msg.text : ''
|
|
71
|
+
if (!text) continue
|
|
72
|
+
const idx = text.toLowerCase().indexOf(needle)
|
|
72
73
|
if (idx === -1) continue
|
|
73
74
|
// Build snippet with context around match
|
|
74
75
|
const start = Math.max(0, idx - 30)
|
|
75
|
-
const end = Math.min(
|
|
76
|
-
const snippet = (start > 0 ? '...' : '') +
|
|
77
|
-
const
|
|
76
|
+
const end = Math.min(text.length, idx + needle.length + 50)
|
|
77
|
+
const snippet = (start > 0 ? '...' : '') + text.slice(start, end).replace(/\n/g, ' ') + (end < text.length ? '...' : '')
|
|
78
|
+
const msgTime = typeof msg.time === 'number' ? msg.time : 0
|
|
79
|
+
const timeAgo = msgTime ? formatTimeAgo(msgTime) : ''
|
|
78
80
|
results.push({
|
|
79
81
|
type: 'message',
|
|
80
82
|
id: sessionId,
|
|
@@ -2,11 +2,57 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { loadSessions, saveSessions } from '@/lib/server/storage'
|
|
3
3
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
4
|
|
|
5
|
-
export async function GET(
|
|
5
|
+
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
6
6
|
const { id } = await params
|
|
7
7
|
const sessions = loadSessions()
|
|
8
8
|
if (!sessions[id]) return notFound()
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
const url = new URL(req.url)
|
|
11
|
+
const limitParam = url.searchParams.get('limit')
|
|
12
|
+
const beforeParam = url.searchParams.get('before')
|
|
13
|
+
|
|
14
|
+
const allMessages = sessions[id].messages
|
|
15
|
+
const total = allMessages.length
|
|
16
|
+
|
|
17
|
+
// If no limit param, return all messages (backward compatible)
|
|
18
|
+
if (!limitParam) {
|
|
19
|
+
return NextResponse.json(allMessages)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const limit = Math.max(1, Math.min(500, parseInt(limitParam) || 100))
|
|
23
|
+
const before = beforeParam !== null ? parseInt(beforeParam) : total
|
|
24
|
+
|
|
25
|
+
// Return `limit` messages ending just before `before` index
|
|
26
|
+
const start = Math.max(0, before - limit)
|
|
27
|
+
const end = Math.max(0, before)
|
|
28
|
+
const messages = allMessages.slice(start, end)
|
|
29
|
+
|
|
30
|
+
return NextResponse.json({
|
|
31
|
+
messages,
|
|
32
|
+
total,
|
|
33
|
+
hasMore: start > 0,
|
|
34
|
+
startIndex: start,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
39
|
+
const { id } = await params
|
|
40
|
+
const body = await req.json() as { kind?: string }
|
|
41
|
+
if (body.kind !== 'context-clear') {
|
|
42
|
+
return NextResponse.json({ error: 'Only context-clear kind is supported' }, { status: 400 })
|
|
43
|
+
}
|
|
44
|
+
const sessions = loadSessions()
|
|
45
|
+
const session = sessions[id]
|
|
46
|
+
if (!session) return notFound()
|
|
47
|
+
|
|
48
|
+
session.messages.push({
|
|
49
|
+
role: 'user',
|
|
50
|
+
text: '',
|
|
51
|
+
kind: 'context-clear',
|
|
52
|
+
time: Date.now(),
|
|
53
|
+
})
|
|
54
|
+
saveSessions(sessions)
|
|
55
|
+
return NextResponse.json({ ok: true })
|
|
10
56
|
}
|
|
11
57
|
|
|
12
58
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
@@ -25,3 +71,25 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
25
71
|
saveSessions(sessions)
|
|
26
72
|
return NextResponse.json(session.messages[messageIndex])
|
|
27
73
|
}
|
|
74
|
+
|
|
75
|
+
export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
76
|
+
const { id } = await params
|
|
77
|
+
const body = await req.json() as { messageIndex: number }
|
|
78
|
+
const sessions = loadSessions()
|
|
79
|
+
const session = sessions[id]
|
|
80
|
+
if (!session) return notFound()
|
|
81
|
+
|
|
82
|
+
const { messageIndex } = body
|
|
83
|
+
if (typeof messageIndex !== 'number' || messageIndex < 0 || messageIndex >= session.messages.length) {
|
|
84
|
+
return NextResponse.json({ error: 'Invalid message index' }, { status: 400 })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Only allow deleting context-clear markers (safety guard)
|
|
88
|
+
if (session.messages[messageIndex].kind !== 'context-clear') {
|
|
89
|
+
return NextResponse.json({ error: 'Only context-clear markers can be removed' }, { status: 400 })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
session.messages.splice(messageIndex, 1)
|
|
93
|
+
saveSessions(sessions)
|
|
94
|
+
return NextResponse.json({ ok: true })
|
|
95
|
+
}
|
|
@@ -69,6 +69,10 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
69
69
|
if (updates.heartbeatEnabled !== undefined) sessions[id].heartbeatEnabled = updates.heartbeatEnabled
|
|
70
70
|
if (updates.heartbeatIntervalSec !== undefined) sessions[id].heartbeatIntervalSec = updates.heartbeatIntervalSec
|
|
71
71
|
if (updates.pinned !== undefined) sessions[id].pinned = !!updates.pinned
|
|
72
|
+
if (updates.claudeSessionId !== undefined) sessions[id].claudeSessionId = updates.claudeSessionId
|
|
73
|
+
if (updates.codexThreadId !== undefined) sessions[id].codexThreadId = updates.codexThreadId
|
|
74
|
+
if (updates.opencodeSessionId !== undefined) sessions[id].opencodeSessionId = updates.opencodeSessionId
|
|
75
|
+
if (updates.delegateResumeIds !== undefined) sessions[id].delegateResumeIds = updates.delegateResumeIds
|
|
72
76
|
if (!Array.isArray(sessions[id].messages)) sessions[id].messages = []
|
|
73
77
|
ensureMainSessionFlag(sessions[id])
|
|
74
78
|
|
|
@@ -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,17 @@ 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,
|
|
73
80
|
projectId: typeof body.projectId === 'string' && body.projectId ? body.projectId : null,
|
|
74
81
|
goalContract: body.goalContract || null,
|
|
75
82
|
cwd: typeof body.cwd === 'string' ? body.cwd : null,
|
|
@@ -94,6 +101,14 @@ export async function POST(req: Request) {
|
|
|
94
101
|
tags: Array.isArray(body.tags) ? body.tags.filter((s: unknown) => typeof s === 'string') : [],
|
|
95
102
|
dueAt: typeof body.dueAt === 'number' ? body.dueAt : null,
|
|
96
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 })
|
|
97
112
|
}
|
|
98
113
|
|
|
99
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: {
|