@swarmclawai/swarmclaw 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import { NextResponse } from 'next/server'
|
|
5
|
+
import { loadAgents, loadCredentials, loadSettings } from '@/lib/server/storage'
|
|
6
|
+
|
|
7
|
+
type CheckStatus = 'pass' | 'warn' | 'fail'
|
|
8
|
+
|
|
9
|
+
interface SetupCheck {
|
|
10
|
+
id: string
|
|
11
|
+
label: string
|
|
12
|
+
status: CheckStatus
|
|
13
|
+
detail: string
|
|
14
|
+
required?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface CommandResult {
|
|
18
|
+
ok: boolean
|
|
19
|
+
output: string
|
|
20
|
+
error?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const RELEASE_TAG_RE = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/
|
|
24
|
+
|
|
25
|
+
function run(command: string, args: string[], timeoutMs = 8_000): CommandResult {
|
|
26
|
+
try {
|
|
27
|
+
const result = spawnSync(command, args, {
|
|
28
|
+
cwd: process.cwd(),
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
timeout: timeoutMs,
|
|
31
|
+
})
|
|
32
|
+
if (result.error) {
|
|
33
|
+
return { ok: false, output: '', error: result.error.message }
|
|
34
|
+
}
|
|
35
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
36
|
+
const err = (result.stderr || result.stdout || `exit ${result.status}`).trim()
|
|
37
|
+
return { ok: false, output: '', error: err || `exit ${result.status}` }
|
|
38
|
+
}
|
|
39
|
+
return { ok: true, output: (result.stdout || '').trim() }
|
|
40
|
+
} catch (err: any) {
|
|
41
|
+
return { ok: false, output: '', error: err?.message || String(err) }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getLatestStableTag(): string | null {
|
|
46
|
+
const listed = run('git', ['tag', '--list', 'v*', '--sort=-v:refname'], 4_000)
|
|
47
|
+
if (!listed.ok) return null
|
|
48
|
+
const tags = listed.output
|
|
49
|
+
.split('\n')
|
|
50
|
+
.map((line) => line.trim())
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
return tags.find((tag) => RELEASE_TAG_RE.test(tag)) || null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function commandExists(name: string): boolean {
|
|
56
|
+
const lookup = process.platform === 'win32' ? 'where' : 'which'
|
|
57
|
+
return run(lookup, [name], 3_000).ok
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function pushCheck(
|
|
61
|
+
checks: SetupCheck[],
|
|
62
|
+
id: string,
|
|
63
|
+
label: string,
|
|
64
|
+
status: CheckStatus,
|
|
65
|
+
detail: string,
|
|
66
|
+
required = false,
|
|
67
|
+
) {
|
|
68
|
+
checks.push({ id, label, status, detail, required })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function testDataWriteAccess(dataDir: string): { ok: boolean; error?: string } {
|
|
72
|
+
try {
|
|
73
|
+
fs.mkdirSync(dataDir, { recursive: true })
|
|
74
|
+
const probe = path.join(dataDir, `.doctor-write-${Date.now()}.tmp`)
|
|
75
|
+
fs.writeFileSync(probe, 'ok', 'utf8')
|
|
76
|
+
fs.unlinkSync(probe)
|
|
77
|
+
return { ok: true }
|
|
78
|
+
} catch (err: any) {
|
|
79
|
+
return { ok: false, error: err?.message || String(err) }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function GET(req: Request) {
|
|
84
|
+
const url = new URL(req.url)
|
|
85
|
+
const includeRemote = url.searchParams.get('remote') === '1'
|
|
86
|
+
const checks: SetupCheck[] = []
|
|
87
|
+
const actions: string[] = []
|
|
88
|
+
const checkedAt = Date.now()
|
|
89
|
+
|
|
90
|
+
const nodeVersion = process.versions.node
|
|
91
|
+
const nodeMajor = Number.parseInt(String(nodeVersion).split('.')[0] || '0', 10)
|
|
92
|
+
if (nodeMajor >= 20) {
|
|
93
|
+
pushCheck(checks, 'node-version', 'Node.js version', 'pass', `Detected Node ${nodeVersion}.`, true)
|
|
94
|
+
} else {
|
|
95
|
+
pushCheck(checks, 'node-version', 'Node.js version', 'fail', `Detected Node ${nodeVersion}. Node 20+ is required.`, true)
|
|
96
|
+
actions.push('Install Node.js 20 or newer from https://nodejs.org and rerun setup.')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const npmCheck = run('npm', ['--version'], 5_000)
|
|
100
|
+
if (npmCheck.ok) {
|
|
101
|
+
pushCheck(checks, 'npm', 'npm availability', 'pass', `npm ${npmCheck.output} is available.`, true)
|
|
102
|
+
} else {
|
|
103
|
+
pushCheck(checks, 'npm', 'npm availability', 'fail', npmCheck.error || 'npm was not found in PATH.', true)
|
|
104
|
+
actions.push('Install npm and rerun `npm run setup:easy`.')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const dataDir = path.join(process.cwd(), 'data')
|
|
108
|
+
const dataWrite = testDataWriteAccess(dataDir)
|
|
109
|
+
if (dataWrite.ok) {
|
|
110
|
+
pushCheck(checks, 'data-dir', 'Data directory permissions', 'pass', `Writable: ${dataDir}`, true)
|
|
111
|
+
} else {
|
|
112
|
+
pushCheck(checks, 'data-dir', 'Data directory permissions', 'fail', dataWrite.error || `Cannot write to ${dataDir}`, true)
|
|
113
|
+
actions.push(`Fix filesystem permissions for ${dataDir}.`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const envFile = path.join(process.cwd(), '.env.local')
|
|
117
|
+
if (fs.existsSync(envFile)) {
|
|
118
|
+
pushCheck(checks, 'env-file', '.env.local', 'pass', '.env.local is present.')
|
|
119
|
+
} else {
|
|
120
|
+
pushCheck(checks, 'env-file', '.env.local', 'warn', '.env.local was not found yet. It will be created automatically on first run.')
|
|
121
|
+
actions.push('Run `npm run dev` once to auto-generate ACCESS_KEY and CREDENTIAL_SECRET.')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const hasAccessKey = !!process.env.ACCESS_KEY?.trim()
|
|
125
|
+
if (hasAccessKey) {
|
|
126
|
+
pushCheck(checks, 'access-key', 'Access key', 'pass', 'ACCESS_KEY is configured.', true)
|
|
127
|
+
} else {
|
|
128
|
+
pushCheck(checks, 'access-key', 'Access key', 'fail', 'ACCESS_KEY is missing.', true)
|
|
129
|
+
actions.push('Start the app once so SwarmClaw can generate ACCESS_KEY automatically.')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const hasCredentialSecret = !!process.env.CREDENTIAL_SECRET?.trim()
|
|
133
|
+
if (hasCredentialSecret) {
|
|
134
|
+
pushCheck(checks, 'credential-secret', 'Credential secret', 'pass', 'CREDENTIAL_SECRET is configured.', true)
|
|
135
|
+
} else {
|
|
136
|
+
pushCheck(checks, 'credential-secret', 'Credential secret', 'fail', 'CREDENTIAL_SECRET is missing.', true)
|
|
137
|
+
actions.push('Start the app once so SwarmClaw can generate CREDENTIAL_SECRET automatically.')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const settings = loadSettings()
|
|
141
|
+
if (settings?.setupCompleted === true) {
|
|
142
|
+
pushCheck(checks, 'setup-wizard', 'Setup wizard', 'pass', 'Initial setup has been completed.')
|
|
143
|
+
} else {
|
|
144
|
+
pushCheck(checks, 'setup-wizard', 'Setup wizard', 'warn', 'Initial setup is not marked complete yet.')
|
|
145
|
+
actions.push('Open the UI and finish the setup wizard at least once.')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const agents = Object.values(loadAgents() || {})
|
|
149
|
+
if (agents.length > 0) {
|
|
150
|
+
pushCheck(checks, 'agents', 'Agents', 'pass', `${agents.length} agent(s) configured.`)
|
|
151
|
+
} else {
|
|
152
|
+
pushCheck(checks, 'agents', 'Agents', 'warn', 'No agents found.')
|
|
153
|
+
actions.push('Create a starter agent from the setup wizard.')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const credentials = Object.values(loadCredentials() || {})
|
|
157
|
+
if (credentials.length > 0) {
|
|
158
|
+
pushCheck(checks, 'credentials', 'Credentials', 'pass', `${credentials.length} credential(s) saved.`)
|
|
159
|
+
} else {
|
|
160
|
+
pushCheck(checks, 'credentials', 'Credentials', 'warn', 'No API credentials saved (OK for local-only Ollama).')
|
|
161
|
+
actions.push('If using cloud providers, add an API key in the setup wizard or Settings → Providers.')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const optionalBinaries: Array<{ id: string; label: string; command: string }> = [
|
|
165
|
+
{ id: 'claude-cli', label: 'Claude Code CLI', command: 'claude' },
|
|
166
|
+
{ id: 'codex-cli', label: 'OpenAI Codex CLI', command: 'codex' },
|
|
167
|
+
{ id: 'opencode-cli', label: 'OpenCode CLI', command: 'opencode' },
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
for (const binary of optionalBinaries) {
|
|
171
|
+
const exists = commandExists(binary.command)
|
|
172
|
+
pushCheck(
|
|
173
|
+
checks,
|
|
174
|
+
binary.id,
|
|
175
|
+
binary.label,
|
|
176
|
+
exists ? 'pass' : 'warn',
|
|
177
|
+
exists
|
|
178
|
+
? `${binary.command} is installed.`
|
|
179
|
+
: `${binary.command} is not installed (optional, only needed for ${binary.label} provider).`,
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const gitRootCheck = run('git', ['rev-parse', '--is-inside-work-tree'], 4_000)
|
|
184
|
+
let localSha: string | null = null
|
|
185
|
+
let remoteSha: string | null = null
|
|
186
|
+
let behindBy = 0
|
|
187
|
+
let workingTreeDirty = false
|
|
188
|
+
|
|
189
|
+
if (!gitRootCheck.ok) {
|
|
190
|
+
pushCheck(checks, 'git-repo', 'Git repository', 'warn', 'This directory is not a git repository. Auto-update checks are disabled.')
|
|
191
|
+
} else {
|
|
192
|
+
pushCheck(checks, 'git-repo', 'Git repository', 'pass', 'Git repository detected.')
|
|
193
|
+
|
|
194
|
+
localSha = run('git', ['rev-parse', '--short', 'HEAD'], 4_000).output || null
|
|
195
|
+
const dirty = run('git', ['status', '--porcelain'], 4_000)
|
|
196
|
+
workingTreeDirty = !!dirty.output
|
|
197
|
+
if (workingTreeDirty) {
|
|
198
|
+
pushCheck(checks, 'git-dirty', 'Working tree cleanliness', 'warn', 'Uncommitted local changes detected.')
|
|
199
|
+
actions.push('Commit or stash local changes before running automatic updates.')
|
|
200
|
+
} else {
|
|
201
|
+
pushCheck(checks, 'git-dirty', 'Working tree cleanliness', 'pass', 'Working tree is clean.')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (includeRemote) {
|
|
205
|
+
const fetch = run('git', ['fetch', '--tags', 'origin', '--quiet'], 12_000)
|
|
206
|
+
if (!fetch.ok) {
|
|
207
|
+
pushCheck(checks, 'git-remote', 'Remote update check', 'warn', fetch.error || 'Could not check remote release tags.')
|
|
208
|
+
} else {
|
|
209
|
+
const latestTag = getLatestStableTag()
|
|
210
|
+
if (!latestTag) {
|
|
211
|
+
pushCheck(checks, 'git-update', 'Update availability', 'warn', 'No stable release tags found yet; updater will fallback to main.')
|
|
212
|
+
} else {
|
|
213
|
+
const behind = run('git', ['rev-list', `HEAD..${latestTag}^{commit}`, '--count'], 4_000)
|
|
214
|
+
behindBy = Number.parseInt(behind.output || '0', 10) || 0
|
|
215
|
+
remoteSha = run('git', ['rev-parse', '--short', `${latestTag}^{commit}`], 4_000).output || localSha
|
|
216
|
+
|
|
217
|
+
if (behindBy > 0) {
|
|
218
|
+
pushCheck(checks, 'git-update', 'Update availability', 'warn', `${behindBy} commit(s) available to stable release ${latestTag}.`)
|
|
219
|
+
actions.push('Run `npm run update:easy` or use the in-app update banner.')
|
|
220
|
+
} else {
|
|
221
|
+
pushCheck(checks, 'git-update', 'Update availability', 'pass', `Already on stable release ${latestTag} or newer.`)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
pushCheck(checks, 'git-remote', 'Remote update check', 'warn', 'Skipped (pass ?remote=1 to include remote stable-tag check).')
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const failedRequired = checks.filter((c) => c.required && c.status === 'fail').length
|
|
231
|
+
const warnings = checks.filter((c) => c.status === 'warn').length
|
|
232
|
+
const ok = failedRequired === 0
|
|
233
|
+
const summary = ok
|
|
234
|
+
? (warnings > 0 ? `Setup mostly healthy with ${warnings} warning(s).` : 'Setup looks healthy.')
|
|
235
|
+
: `Setup has ${failedRequired} required failure(s).`
|
|
236
|
+
|
|
237
|
+
return NextResponse.json({
|
|
238
|
+
ok,
|
|
239
|
+
checkedAt,
|
|
240
|
+
summary,
|
|
241
|
+
checks,
|
|
242
|
+
actions: Array.from(new Set(actions)),
|
|
243
|
+
git: {
|
|
244
|
+
localSha,
|
|
245
|
+
remoteSha,
|
|
246
|
+
behindBy,
|
|
247
|
+
dirty: workingTreeDirty,
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadSkills, saveSkills, deleteSkill } from '@/lib/server/storage'
|
|
3
|
+
import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
|
|
4
|
+
|
|
5
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
6
|
+
const { id } = await params
|
|
7
|
+
const skills = loadSkills()
|
|
8
|
+
if (!skills[id]) return new NextResponse(null, { status: 404 })
|
|
9
|
+
return NextResponse.json(skills[id])
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
13
|
+
const { id } = await params
|
|
14
|
+
const body = await req.json()
|
|
15
|
+
const skills = loadSkills()
|
|
16
|
+
if (!skills[id]) return new NextResponse(null, { status: 404 })
|
|
17
|
+
const normalized = normalizeSkillPayload({ ...skills[id], ...body })
|
|
18
|
+
skills[id] = {
|
|
19
|
+
...skills[id],
|
|
20
|
+
...body,
|
|
21
|
+
name: normalized.name,
|
|
22
|
+
filename: normalized.filename,
|
|
23
|
+
description: normalized.description,
|
|
24
|
+
content: normalized.content,
|
|
25
|
+
sourceUrl: normalized.sourceUrl,
|
|
26
|
+
sourceFormat: normalized.sourceFormat,
|
|
27
|
+
id,
|
|
28
|
+
updatedAt: Date.now(),
|
|
29
|
+
}
|
|
30
|
+
saveSkills(skills)
|
|
31
|
+
return NextResponse.json(skills[id])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
35
|
+
const { id } = await params
|
|
36
|
+
const skills = loadSkills()
|
|
37
|
+
if (!skills[id]) return new NextResponse(null, { status: 404 })
|
|
38
|
+
deleteSkill(id)
|
|
39
|
+
return NextResponse.json({ deleted: id })
|
|
40
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import { NextResponse } from 'next/server'
|
|
3
|
+
import { loadSkills, saveSkills } from '@/lib/server/storage'
|
|
4
|
+
import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
|
|
5
|
+
|
|
6
|
+
const MAX_SKILL_BYTES = 2 * 1024 * 1024
|
|
7
|
+
|
|
8
|
+
function validateHttpUrl(value: unknown): string {
|
|
9
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
10
|
+
throw new Error('url is required')
|
|
11
|
+
}
|
|
12
|
+
const parsed = new URL(value.trim())
|
|
13
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
14
|
+
throw new Error('Only http/https URLs are supported')
|
|
15
|
+
}
|
|
16
|
+
return parsed.toString()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function POST(req: Request) {
|
|
20
|
+
try {
|
|
21
|
+
const body = await req.json()
|
|
22
|
+
const url = validateHttpUrl(body.url)
|
|
23
|
+
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
signal: AbortSignal.timeout(20_000),
|
|
26
|
+
headers: {
|
|
27
|
+
'User-Agent': 'SwarmClaw/1.0 skill-import',
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw new Error(`Failed to fetch skill (${res.status})`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const content = await res.text()
|
|
36
|
+
if (!content.trim()) {
|
|
37
|
+
throw new Error('Fetched skill file is empty')
|
|
38
|
+
}
|
|
39
|
+
if (Buffer.byteLength(content, 'utf8') > MAX_SKILL_BYTES) {
|
|
40
|
+
throw new Error('Skill file too large (max 2MB)')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const normalized = normalizeSkillPayload({
|
|
44
|
+
...body,
|
|
45
|
+
content,
|
|
46
|
+
sourceUrl: url,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const skills = loadSkills()
|
|
50
|
+
const id = crypto.randomBytes(4).toString('hex')
|
|
51
|
+
skills[id] = {
|
|
52
|
+
id,
|
|
53
|
+
name: normalized.name,
|
|
54
|
+
filename: normalized.filename,
|
|
55
|
+
description: normalized.description,
|
|
56
|
+
content: normalized.content,
|
|
57
|
+
sourceUrl: normalized.sourceUrl,
|
|
58
|
+
sourceFormat: normalized.sourceFormat,
|
|
59
|
+
createdAt: Date.now(),
|
|
60
|
+
updatedAt: Date.now(),
|
|
61
|
+
}
|
|
62
|
+
saveSkills(skills)
|
|
63
|
+
|
|
64
|
+
return NextResponse.json(skills[id])
|
|
65
|
+
} catch (err: unknown) {
|
|
66
|
+
const message = err instanceof Error ? err.message : 'Failed to import skill'
|
|
67
|
+
return NextResponse.json({ error: message }, { status: 400 })
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import crypto from 'crypto'
|
|
3
|
+
import { loadSkills, saveSkills } from '@/lib/server/storage'
|
|
4
|
+
import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
return NextResponse.json(loadSkills())
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function POST(req: Request) {
|
|
11
|
+
const body = await req.json()
|
|
12
|
+
const skills = loadSkills()
|
|
13
|
+
const id = crypto.randomBytes(4).toString('hex')
|
|
14
|
+
const normalized = normalizeSkillPayload(body)
|
|
15
|
+
skills[id] = {
|
|
16
|
+
id,
|
|
17
|
+
name: normalized.name,
|
|
18
|
+
filename: normalized.filename || `skill-${id}.md`,
|
|
19
|
+
content: normalized.content || '',
|
|
20
|
+
description: normalized.description || '',
|
|
21
|
+
sourceUrl: normalized.sourceUrl,
|
|
22
|
+
sourceFormat: normalized.sourceFormat,
|
|
23
|
+
createdAt: Date.now(),
|
|
24
|
+
updatedAt: Date.now(),
|
|
25
|
+
}
|
|
26
|
+
saveSkills(skills)
|
|
27
|
+
return NextResponse.json(skills[id])
|
|
28
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import { NextResponse } from 'next/server'
|
|
3
|
+
import { loadTasks, saveTasks } from '@/lib/server/storage'
|
|
4
|
+
import { disableSessionHeartbeat, enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
|
|
5
|
+
import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
|
|
6
|
+
import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
|
|
7
|
+
import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
|
|
8
|
+
|
|
9
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
10
|
+
// Keep completed queue integrity even if daemon is not running.
|
|
11
|
+
validateCompletedTasksQueue()
|
|
12
|
+
|
|
13
|
+
const { id } = await params
|
|
14
|
+
const tasks = loadTasks()
|
|
15
|
+
if (!tasks[id]) return new NextResponse(null, { status: 404 })
|
|
16
|
+
return NextResponse.json(tasks[id])
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
20
|
+
const { id } = await params
|
|
21
|
+
const body = await req.json()
|
|
22
|
+
const tasks = loadTasks()
|
|
23
|
+
if (!tasks[id]) return new NextResponse(null, { status: 404 })
|
|
24
|
+
|
|
25
|
+
const prevStatus = tasks[id].status
|
|
26
|
+
|
|
27
|
+
// Support atomic comment append to avoid race conditions
|
|
28
|
+
if (body.appendComment) {
|
|
29
|
+
if (!tasks[id].comments) tasks[id].comments = []
|
|
30
|
+
tasks[id].comments.push(body.appendComment)
|
|
31
|
+
tasks[id].updatedAt = Date.now()
|
|
32
|
+
} else {
|
|
33
|
+
Object.assign(tasks[id], body, { updatedAt: Date.now() })
|
|
34
|
+
}
|
|
35
|
+
tasks[id].id = id // prevent id overwrite
|
|
36
|
+
|
|
37
|
+
// Set archivedAt when transitioning to archived
|
|
38
|
+
if (prevStatus !== 'archived' && tasks[id].status === 'archived') {
|
|
39
|
+
tasks[id].archivedAt = Date.now()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Re-validate any completed task updates so "completed" always means actually done.
|
|
43
|
+
if (tasks[id].status === 'completed') {
|
|
44
|
+
const report = ensureTaskCompletionReport(tasks[id])
|
|
45
|
+
if (report?.relativePath) tasks[id].completionReportPath = report.relativePath
|
|
46
|
+
const validation = validateTaskCompletion(tasks[id], { report })
|
|
47
|
+
tasks[id].validation = validation
|
|
48
|
+
if (validation.ok) {
|
|
49
|
+
tasks[id].completedAt = tasks[id].completedAt || Date.now()
|
|
50
|
+
tasks[id].error = null
|
|
51
|
+
} else {
|
|
52
|
+
tasks[id].status = 'failed'
|
|
53
|
+
tasks[id].completedAt = null
|
|
54
|
+
tasks[id].error = formatValidationFailure(validation.reasons).slice(0, 500)
|
|
55
|
+
if (!tasks[id].comments) tasks[id].comments = []
|
|
56
|
+
tasks[id].comments.push({
|
|
57
|
+
id: crypto.randomBytes(4).toString('hex'),
|
|
58
|
+
author: 'System',
|
|
59
|
+
text: `Completion validation failed.\n\n${validation.reasons.map((r) => `- ${r}`).join('\n')}`,
|
|
60
|
+
createdAt: Date.now(),
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
saveTasks(tasks)
|
|
66
|
+
if (prevStatus !== tasks[id].status) {
|
|
67
|
+
pushMainLoopEventToMainSessions({
|
|
68
|
+
type: 'task_status_changed',
|
|
69
|
+
text: `Task "${tasks[id].title}" (${id}) moved ${prevStatus} → ${tasks[id].status}.`,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// If task is manually transitioned to a terminal status, disable session heartbeat.
|
|
74
|
+
if (prevStatus !== tasks[id].status && (tasks[id].status === 'completed' || tasks[id].status === 'failed')) {
|
|
75
|
+
disableSessionHeartbeat(tasks[id].sessionId)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// If status changed to 'queued', enqueue it
|
|
79
|
+
if (prevStatus !== 'queued' && tasks[id].status === 'queued') {
|
|
80
|
+
enqueueTask(id)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return NextResponse.json(tasks[id])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
87
|
+
const { id } = await params
|
|
88
|
+
const tasks = loadTasks()
|
|
89
|
+
if (!tasks[id]) return new NextResponse(null, { status: 404 })
|
|
90
|
+
|
|
91
|
+
// Soft delete: move to archived status instead of hard delete
|
|
92
|
+
tasks[id].status = 'archived'
|
|
93
|
+
tasks[id].archivedAt = Date.now()
|
|
94
|
+
tasks[id].updatedAt = Date.now()
|
|
95
|
+
saveTasks(tasks)
|
|
96
|
+
pushMainLoopEventToMainSessions({
|
|
97
|
+
type: 'task_archived',
|
|
98
|
+
text: `Task archived: "${tasks[id].title}" (${id}).`,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
return NextResponse.json(tasks[id])
|
|
102
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import crypto from 'crypto'
|
|
3
|
+
import { loadTasks, saveTasks, loadSettings } from '@/lib/server/storage'
|
|
4
|
+
import { enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
|
|
5
|
+
import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
|
|
6
|
+
import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
|
|
7
|
+
import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
|
|
8
|
+
|
|
9
|
+
export async function GET(req: Request) {
|
|
10
|
+
// Keep completed queue integrity even if daemon is not running.
|
|
11
|
+
validateCompletedTasksQueue()
|
|
12
|
+
|
|
13
|
+
const { searchParams } = new URL(req.url)
|
|
14
|
+
const includeArchived = searchParams.get('includeArchived') === 'true'
|
|
15
|
+
const allTasks = loadTasks()
|
|
16
|
+
|
|
17
|
+
if (includeArchived) {
|
|
18
|
+
return NextResponse.json(allTasks)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Exclude archived tasks by default
|
|
22
|
+
const filtered: Record<string, typeof allTasks[string]> = {}
|
|
23
|
+
for (const [id, task] of Object.entries(allTasks)) {
|
|
24
|
+
if (task.status !== 'archived') {
|
|
25
|
+
filtered[id] = task
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return NextResponse.json(filtered)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function DELETE(req: Request) {
|
|
32
|
+
const { searchParams } = new URL(req.url)
|
|
33
|
+
const filter = searchParams.get('filter') // 'all' | 'schedule' | 'done' | null
|
|
34
|
+
const tasks = loadTasks()
|
|
35
|
+
let removed = 0
|
|
36
|
+
|
|
37
|
+
const shouldRemove = (task: { status: string; sourceType?: string }) =>
|
|
38
|
+
filter === 'all' ||
|
|
39
|
+
(filter === 'schedule' && task.sourceType === 'schedule') ||
|
|
40
|
+
(filter === 'done' && (task.status === 'completed' || task.status === 'failed')) ||
|
|
41
|
+
(!filter && task.status === 'archived')
|
|
42
|
+
|
|
43
|
+
const { deleteTask } = await import('@/lib/server/storage')
|
|
44
|
+
for (const [id, task] of Object.entries(tasks)) {
|
|
45
|
+
if (shouldRemove(task as { status: string; sourceType?: string })) {
|
|
46
|
+
deleteTask(id)
|
|
47
|
+
removed++
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return NextResponse.json({ removed, remaining: Object.keys(tasks).length - removed })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function POST(req: Request) {
|
|
54
|
+
const body = await req.json()
|
|
55
|
+
const id = crypto.randomBytes(4).toString('hex')
|
|
56
|
+
const now = Date.now()
|
|
57
|
+
const tasks = loadTasks()
|
|
58
|
+
const settings = loadSettings()
|
|
59
|
+
const maxAttempts = Number.isFinite(Number(body.maxAttempts))
|
|
60
|
+
? Math.max(1, Math.min(20, Math.trunc(Number(body.maxAttempts))))
|
|
61
|
+
: Math.max(1, Math.min(20, Math.trunc(Number(settings.defaultTaskMaxAttempts ?? 3))))
|
|
62
|
+
const retryBackoffSec = Number.isFinite(Number(body.retryBackoffSec))
|
|
63
|
+
? Math.max(1, Math.min(3600, Math.trunc(Number(body.retryBackoffSec))))
|
|
64
|
+
: Math.max(1, Math.min(3600, Math.trunc(Number(settings.taskRetryBackoffSec ?? 30))))
|
|
65
|
+
tasks[id] = {
|
|
66
|
+
id,
|
|
67
|
+
title: body.title || 'Untitled Task',
|
|
68
|
+
description: body.description || '',
|
|
69
|
+
status: body.status || 'backlog',
|
|
70
|
+
agentId: body.agentId || '',
|
|
71
|
+
goalContract: body.goalContract || null,
|
|
72
|
+
cwd: typeof body.cwd === 'string' ? body.cwd : null,
|
|
73
|
+
file: typeof body.file === 'string' ? body.file : null,
|
|
74
|
+
sessionId: typeof body.sessionId === 'string' ? body.sessionId : null,
|
|
75
|
+
result: typeof body.result === 'string' ? body.result : null,
|
|
76
|
+
error: typeof body.error === 'string' ? body.error : null,
|
|
77
|
+
createdAt: now,
|
|
78
|
+
updatedAt: now,
|
|
79
|
+
queuedAt: null,
|
|
80
|
+
startedAt: null,
|
|
81
|
+
completedAt: null,
|
|
82
|
+
archivedAt: null,
|
|
83
|
+
attempts: 0,
|
|
84
|
+
maxAttempts,
|
|
85
|
+
retryBackoffSec,
|
|
86
|
+
retryScheduledAt: null,
|
|
87
|
+
deadLetteredAt: null,
|
|
88
|
+
checkpoint: null,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (tasks[id].status === 'completed') {
|
|
92
|
+
const report = ensureTaskCompletionReport(tasks[id])
|
|
93
|
+
if (report?.relativePath) tasks[id].completionReportPath = report.relativePath
|
|
94
|
+
const validation = validateTaskCompletion(tasks[id], { report })
|
|
95
|
+
tasks[id].validation = validation
|
|
96
|
+
if (validation.ok) {
|
|
97
|
+
tasks[id].completedAt = Date.now()
|
|
98
|
+
tasks[id].error = null
|
|
99
|
+
} else {
|
|
100
|
+
tasks[id].status = 'failed'
|
|
101
|
+
tasks[id].completedAt = null
|
|
102
|
+
tasks[id].error = formatValidationFailure(validation.reasons).slice(0, 500)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
saveTasks(tasks)
|
|
107
|
+
pushMainLoopEventToMainSessions({
|
|
108
|
+
type: 'task_created',
|
|
109
|
+
text: `Task created: "${tasks[id].title}" (${id}) with status ${tasks[id].status}.`,
|
|
110
|
+
})
|
|
111
|
+
if (tasks[id].status === 'queued') {
|
|
112
|
+
enqueueTask(id)
|
|
113
|
+
}
|
|
114
|
+
return NextResponse.json(tasks[id])
|
|
115
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadSettings } from '@/lib/server/storage'
|
|
3
|
+
|
|
4
|
+
export async function POST(req: Request) {
|
|
5
|
+
const settings = loadSettings()
|
|
6
|
+
const ELEVENLABS_KEY = settings.elevenLabsApiKey || process.env.ELEVENLABS_API_KEY
|
|
7
|
+
const ELEVENLABS_VOICE = settings.elevenLabsVoiceId || process.env.ELEVENLABS_VOICE || 'JBFqnCBsd6RMkjVDRZzb'
|
|
8
|
+
|
|
9
|
+
if (!ELEVENLABS_KEY) {
|
|
10
|
+
return new NextResponse('No ElevenLabs API key. Set one in Settings > Voice.', { status: 500 })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { text } = await req.json()
|
|
14
|
+
const apiRes = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${ELEVENLABS_VOICE}`, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: {
|
|
17
|
+
'xi-api-key': ELEVENLABS_KEY,
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
'Accept': 'audio/mpeg',
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
text,
|
|
23
|
+
model_id: 'eleven_multilingual_v2',
|
|
24
|
+
voice_settings: { stability: 0.5, similarity_boost: 0.75 },
|
|
25
|
+
}),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
if (!apiRes.ok) {
|
|
29
|
+
const err = await apiRes.text()
|
|
30
|
+
return new NextResponse(err, { status: apiRes.status })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const audioBuffer = await apiRes.arrayBuffer()
|
|
34
|
+
return new NextResponse(audioBuffer, {
|
|
35
|
+
headers: {
|
|
36
|
+
'Content-Type': 'audio/mpeg',
|
|
37
|
+
'Cache-Control': 'no-cache',
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import crypto from 'crypto'
|
|
5
|
+
import { UPLOAD_DIR } from '@/lib/server/storage'
|
|
6
|
+
|
|
7
|
+
export async function POST(req: Request) {
|
|
8
|
+
const filename = req.headers.get('x-filename') || 'image.png'
|
|
9
|
+
const buf = Buffer.from(await req.arrayBuffer())
|
|
10
|
+
const name = crypto.randomBytes(4).toString('hex') + '-' + filename.replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
11
|
+
const filePath = path.join(UPLOAD_DIR, name)
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
|
|
14
|
+
fs.writeFileSync(filePath, buf)
|
|
15
|
+
console.log(`[upload] saved ${buf.length} bytes to ${filePath}`)
|
|
16
|
+
|
|
17
|
+
return NextResponse.json({ path: filePath, size: buf.length, url: `/api/uploads/${name}` })
|
|
18
|
+
}
|