@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,364 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import {
|
|
3
|
+
loadSessions, saveSessions, loadAgents,
|
|
4
|
+
loadCredentials, decryptKey, loadSettings, loadSkills,
|
|
5
|
+
} from './storage'
|
|
6
|
+
import { loadRuntimeSettings, getLegacyOrchestratorMaxTurns } from './runtime-settings'
|
|
7
|
+
import { getMemoryDb } from './memory-db'
|
|
8
|
+
import { getProvider } from '../providers'
|
|
9
|
+
import type { Agent } from '@/types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates the orchestrator session and returns its ID immediately.
|
|
13
|
+
* Call executeOrchestrator() separately to run the loop in the background.
|
|
14
|
+
*/
|
|
15
|
+
export function createOrchestratorSession(
|
|
16
|
+
orchestrator: Agent,
|
|
17
|
+
task: string,
|
|
18
|
+
parentSessionId?: string,
|
|
19
|
+
cwd?: string,
|
|
20
|
+
): string {
|
|
21
|
+
const sessions = loadSessions()
|
|
22
|
+
const sessionId = crypto.randomBytes(4).toString('hex')
|
|
23
|
+
sessions[sessionId] = {
|
|
24
|
+
id: sessionId,
|
|
25
|
+
name: `[Orch] ${orchestrator.name}: ${task.slice(0, 40)}`,
|
|
26
|
+
cwd: cwd || process.cwd(),
|
|
27
|
+
user: 'system',
|
|
28
|
+
provider: orchestrator.provider,
|
|
29
|
+
model: orchestrator.model,
|
|
30
|
+
credentialId: orchestrator.credentialId || null,
|
|
31
|
+
apiEndpoint: orchestrator.apiEndpoint || null,
|
|
32
|
+
claudeSessionId: null,
|
|
33
|
+
codexThreadId: null,
|
|
34
|
+
opencodeSessionId: null,
|
|
35
|
+
delegateResumeIds: {
|
|
36
|
+
claudeCode: null,
|
|
37
|
+
codex: null,
|
|
38
|
+
opencode: null,
|
|
39
|
+
},
|
|
40
|
+
messages: [] as any[],
|
|
41
|
+
createdAt: Date.now(),
|
|
42
|
+
lastActiveAt: Date.now(),
|
|
43
|
+
sessionType: 'orchestrated' as const,
|
|
44
|
+
agentId: orchestrator.id,
|
|
45
|
+
parentSessionId: parentSessionId || null,
|
|
46
|
+
tools: Array.isArray(orchestrator.tools) ? [...orchestrator.tools] : [],
|
|
47
|
+
heartbeatEnabled: false,
|
|
48
|
+
}
|
|
49
|
+
saveSessions(sessions)
|
|
50
|
+
return sessionId
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function runOrchestrator(
|
|
54
|
+
orchestrator: Agent,
|
|
55
|
+
task: string,
|
|
56
|
+
parentSessionId?: string,
|
|
57
|
+
): Promise<string> {
|
|
58
|
+
const sessionId = createOrchestratorSession(orchestrator, task, parentSessionId)
|
|
59
|
+
return executeOrchestrator(orchestrator, task, sessionId)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function executeOrchestrator(
|
|
63
|
+
orchestrator: Agent,
|
|
64
|
+
task: string,
|
|
65
|
+
sessionId: string,
|
|
66
|
+
): Promise<string> {
|
|
67
|
+
// Use LangGraph for all non-CLI providers (including OpenAI-compatible custom providers)
|
|
68
|
+
const isCliProvider = orchestrator.provider === 'claude-cli' || orchestrator.provider === 'codex-cli' || orchestrator.provider === 'opencode-cli'
|
|
69
|
+
if (!isCliProvider) {
|
|
70
|
+
console.log(`[orchestrator] Using LangGraph engine for ${orchestrator.name} (${orchestrator.provider})`)
|
|
71
|
+
const { executeLangGraphOrchestrator } = await import('./orchestrator-lg')
|
|
72
|
+
return executeLangGraphOrchestrator(orchestrator, task, sessionId)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// claude-cli fallback (no structured tool calling)
|
|
76
|
+
console.warn(`[orchestrator] Using legacy regex-based engine for ${orchestrator.name} (claude-cli)`)
|
|
77
|
+
return executeOrchestratorLegacy(orchestrator, task, sessionId)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function executeOrchestratorLegacy(
|
|
81
|
+
orchestrator: Agent,
|
|
82
|
+
task: string,
|
|
83
|
+
sessionId: string,
|
|
84
|
+
): Promise<string> {
|
|
85
|
+
const allAgents = loadAgents()
|
|
86
|
+
const sessions = loadSessions()
|
|
87
|
+
const session = sessions[sessionId]
|
|
88
|
+
if (!session) throw new Error('Orchestrator session not found')
|
|
89
|
+
|
|
90
|
+
// Build available agents list
|
|
91
|
+
const agentIds = orchestrator.subAgentIds || []
|
|
92
|
+
const agents = agentIds.map((id) => allAgents[id]).filter(Boolean)
|
|
93
|
+
const agentList = agents.map((a) => {
|
|
94
|
+
const tools = a.tools?.length ? ` [tools: ${a.tools.join(', ')}]` : ''
|
|
95
|
+
const skills = a.skills?.length ? ` [skills: ${a.skills.join(', ')}]` : ''
|
|
96
|
+
return `- ${a.name}: ${a.description}${tools}${skills}`
|
|
97
|
+
}).join('\n')
|
|
98
|
+
|
|
99
|
+
// Load relevant memories
|
|
100
|
+
const db = getMemoryDb()
|
|
101
|
+
const memories = db.getByAgent(orchestrator.id)
|
|
102
|
+
const memoryContext = memories.length
|
|
103
|
+
? '\n\nRelevant memories:\n' + memories.slice(0, 10).map((m) => `[${m.category}] ${m.title}: ${m.content.slice(0, 200)}`).join('\n')
|
|
104
|
+
: ''
|
|
105
|
+
|
|
106
|
+
// Build system prompt: [userPrompt] \n\n [soul] \n\n [systemPrompt] \n\n [orchestrator context]
|
|
107
|
+
const settings = loadSettings()
|
|
108
|
+
const promptParts: string[] = []
|
|
109
|
+
if (settings.userPrompt) promptParts.push(settings.userPrompt)
|
|
110
|
+
if (orchestrator.soul) promptParts.push(orchestrator.soul)
|
|
111
|
+
if (orchestrator.systemPrompt) promptParts.push(orchestrator.systemPrompt)
|
|
112
|
+
if (orchestrator.skillIds?.length) {
|
|
113
|
+
const allSkills = loadSkills()
|
|
114
|
+
for (const skillId of orchestrator.skillIds) {
|
|
115
|
+
const skill = allSkills[skillId]
|
|
116
|
+
if (skill?.content) promptParts.push(`## Skill: ${skill.name}\n${skill.content}`)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const basePrompt = promptParts.join('\n\n')
|
|
120
|
+
|
|
121
|
+
const systemPrompt = [
|
|
122
|
+
basePrompt,
|
|
123
|
+
'\n\nYou are an orchestrator agent. You can delegate tasks to these agents:',
|
|
124
|
+
agentList || '(no agents available)',
|
|
125
|
+
'\n\nTo delegate a task, output a JSON block on its own line:',
|
|
126
|
+
'{"delegate": {"agent": "agent-name", "task": "what to do"}}',
|
|
127
|
+
'\n\nTo store something in memory:',
|
|
128
|
+
'{"memory_store": {"category": "keyword", "title": "...", "content": "..."}}',
|
|
129
|
+
'\n\nTo read memories:',
|
|
130
|
+
'{"memory_read": {"query": "search terms"}}',
|
|
131
|
+
'\n\nAgents with [tools: browser] have access to a Playwright browser and can navigate websites, scrape data, fill forms, take screenshots, and interact with web pages.',
|
|
132
|
+
'\n\nWhen you are done, output: {"done": true, "summary": "what was accomplished"}',
|
|
133
|
+
memoryContext,
|
|
134
|
+
].join('\n')
|
|
135
|
+
|
|
136
|
+
// Conversation loop
|
|
137
|
+
const conversationHistory: { role: string; text: string }[] = []
|
|
138
|
+
conversationHistory.push({ role: 'user', text: task })
|
|
139
|
+
|
|
140
|
+
let result = ''
|
|
141
|
+
const runtime = loadRuntimeSettings()
|
|
142
|
+
const maxTurns = getLegacyOrchestratorMaxTurns(runtime)
|
|
143
|
+
const loopStart = Date.now()
|
|
144
|
+
|
|
145
|
+
for (let turn = 0; turn < maxTurns; turn++) {
|
|
146
|
+
if (runtime.loopMode === 'ongoing' && runtime.ongoingLoopMaxRuntimeMs) {
|
|
147
|
+
const elapsed = Date.now() - loopStart
|
|
148
|
+
if (elapsed >= runtime.ongoingLoopMaxRuntimeMs) {
|
|
149
|
+
const timeoutMsg = 'Ongoing loop stopped after reaching the configured runtime limit.'
|
|
150
|
+
session.messages.push({ role: 'assistant' as const, text: timeoutMsg, time: Date.now() })
|
|
151
|
+
session.lastActiveAt = Date.now()
|
|
152
|
+
const s = loadSessions()
|
|
153
|
+
s[sessionId] = session
|
|
154
|
+
saveSessions(s)
|
|
155
|
+
return timeoutMsg
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const fullText = await callProvider(orchestrator, systemPrompt, conversationHistory)
|
|
160
|
+
conversationHistory.push({ role: 'assistant', text: fullText })
|
|
161
|
+
|
|
162
|
+
// Save to session
|
|
163
|
+
session.messages.push({ role: 'user' as const, text: turn === 0 ? task : '[system response]', time: Date.now() })
|
|
164
|
+
session.messages.push({ role: 'assistant' as const, text: fullText, time: Date.now() })
|
|
165
|
+
session.lastActiveAt = Date.now()
|
|
166
|
+
const s = loadSessions()
|
|
167
|
+
s[sessionId] = session
|
|
168
|
+
saveSessions(s)
|
|
169
|
+
|
|
170
|
+
// Parse JSON commands from the response
|
|
171
|
+
const commands = extractJsonCommands(fullText)
|
|
172
|
+
let hasDelegate = false
|
|
173
|
+
|
|
174
|
+
for (const cmd of commands) {
|
|
175
|
+
if (cmd.delegate) {
|
|
176
|
+
hasDelegate = true
|
|
177
|
+
const agent = agents.find((a) => a.name.toLowerCase() === cmd.delegate.agent.toLowerCase())
|
|
178
|
+
if (!agent) {
|
|
179
|
+
conversationHistory.push({
|
|
180
|
+
role: 'user',
|
|
181
|
+
text: `[System] Agent "${cmd.delegate.agent}" not found. Available agents: ${agents.map((a) => a.name).join(', ')}`,
|
|
182
|
+
})
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Execute sub-task
|
|
187
|
+
const subResult = await executeSubTask(agent, cmd.delegate.task, sessionId)
|
|
188
|
+
conversationHistory.push({
|
|
189
|
+
role: 'user',
|
|
190
|
+
text: `[Agent ${agent.name} result]:\n${subResult}`,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (cmd.memory_store) {
|
|
195
|
+
db.add({
|
|
196
|
+
agentId: orchestrator.id,
|
|
197
|
+
sessionId,
|
|
198
|
+
category: cmd.memory_store.category || 'note',
|
|
199
|
+
title: cmd.memory_store.title || 'Untitled',
|
|
200
|
+
content: cmd.memory_store.content || '',
|
|
201
|
+
})
|
|
202
|
+
conversationHistory.push({
|
|
203
|
+
role: 'user',
|
|
204
|
+
text: '[System] Memory stored successfully.',
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (cmd.memory_read) {
|
|
209
|
+
const results = db.search(cmd.memory_read.query, orchestrator.id)
|
|
210
|
+
const memText = results.length
|
|
211
|
+
? results.map((m) => `[${m.category}] ${m.title}: ${m.content.slice(0, 300)}`).join('\n')
|
|
212
|
+
: 'No matching memories found.'
|
|
213
|
+
conversationHistory.push({
|
|
214
|
+
role: 'user',
|
|
215
|
+
text: `[Memory search results]:\n${memText}`,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (cmd.done) {
|
|
220
|
+
result = cmd.summary || fullText
|
|
221
|
+
return result
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!hasDelegate && commands.length === 0) {
|
|
226
|
+
// No commands found, treat as final response
|
|
227
|
+
result = fullText
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!result) {
|
|
233
|
+
result = `Loop stopped after reaching max turns (${maxTurns}).`
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return result
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function executeSubTask(
|
|
240
|
+
agent: Agent,
|
|
241
|
+
task: string,
|
|
242
|
+
parentSessionId: string,
|
|
243
|
+
): Promise<string> {
|
|
244
|
+
// Look up parent session cwd to inherit
|
|
245
|
+
const sessions = loadSessions()
|
|
246
|
+
const parentSession = sessions[parentSessionId]
|
|
247
|
+
const childId = crypto.randomBytes(4).toString('hex')
|
|
248
|
+
const childSession = {
|
|
249
|
+
id: childId,
|
|
250
|
+
name: `[Agent] ${agent.name}: ${task.slice(0, 40)}`,
|
|
251
|
+
cwd: parentSession?.cwd || process.cwd(),
|
|
252
|
+
user: 'system',
|
|
253
|
+
provider: agent.provider,
|
|
254
|
+
model: agent.model,
|
|
255
|
+
credentialId: agent.credentialId || null,
|
|
256
|
+
apiEndpoint: agent.apiEndpoint || null,
|
|
257
|
+
claudeSessionId: null,
|
|
258
|
+
codexThreadId: null,
|
|
259
|
+
opencodeSessionId: null,
|
|
260
|
+
delegateResumeIds: {
|
|
261
|
+
claudeCode: null,
|
|
262
|
+
codex: null,
|
|
263
|
+
opencode: null,
|
|
264
|
+
},
|
|
265
|
+
messages: [] as any[],
|
|
266
|
+
createdAt: Date.now(),
|
|
267
|
+
lastActiveAt: Date.now(),
|
|
268
|
+
sessionType: 'orchestrated' as const,
|
|
269
|
+
agentId: agent.id,
|
|
270
|
+
parentSessionId,
|
|
271
|
+
tools: agent.tools || [],
|
|
272
|
+
}
|
|
273
|
+
sessions[childId] = childSession
|
|
274
|
+
saveSessions(sessions)
|
|
275
|
+
|
|
276
|
+
const history = [{ role: 'user', text: task }]
|
|
277
|
+
const result = await callProvider(agent, agent.systemPrompt, history)
|
|
278
|
+
|
|
279
|
+
childSession.messages.push({ role: 'user', text: task, time: Date.now() })
|
|
280
|
+
childSession.messages.push({ role: 'assistant', text: result, time: Date.now() })
|
|
281
|
+
childSession.lastActiveAt = Date.now()
|
|
282
|
+
const s = loadSessions()
|
|
283
|
+
s[childId] = childSession
|
|
284
|
+
saveSessions(s)
|
|
285
|
+
|
|
286
|
+
return result
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function callProvider(
|
|
290
|
+
agent: Agent,
|
|
291
|
+
systemPrompt: string,
|
|
292
|
+
history: { role: string; text: string }[],
|
|
293
|
+
): Promise<string> {
|
|
294
|
+
const provider = getProvider(agent.provider)
|
|
295
|
+
if (!provider) throw new Error(`Unknown provider: ${agent.provider}`)
|
|
296
|
+
|
|
297
|
+
let apiKey: string | null = null
|
|
298
|
+
if (agent.credentialId) {
|
|
299
|
+
const creds = loadCredentials()
|
|
300
|
+
const cred = creds[agent.credentialId]
|
|
301
|
+
if (cred?.encryptedKey) {
|
|
302
|
+
try { apiKey = decryptKey(cred.encryptedKey) } catch { /* ignore */ }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Build a mock session for the provider
|
|
307
|
+
const mockSession = {
|
|
308
|
+
id: 'orch-' + crypto.randomBytes(2).toString('hex'),
|
|
309
|
+
provider: agent.provider,
|
|
310
|
+
model: agent.model,
|
|
311
|
+
credentialId: agent.credentialId,
|
|
312
|
+
apiEndpoint: agent.apiEndpoint,
|
|
313
|
+
cwd: process.cwd(),
|
|
314
|
+
tools: agent.tools || [],
|
|
315
|
+
messages: history.map((h) => ({
|
|
316
|
+
role: h.role as 'user' | 'assistant',
|
|
317
|
+
text: h.text,
|
|
318
|
+
time: Date.now(),
|
|
319
|
+
})),
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let fullText = ''
|
|
323
|
+
const { active } = await import('./storage')
|
|
324
|
+
|
|
325
|
+
await provider.handler.streamChat({
|
|
326
|
+
session: mockSession,
|
|
327
|
+
message: history[history.length - 1].text,
|
|
328
|
+
apiKey,
|
|
329
|
+
write: (data: string) => {
|
|
330
|
+
// Parse SSE data to extract text
|
|
331
|
+
if (data.startsWith('data: ')) {
|
|
332
|
+
try {
|
|
333
|
+
const event = JSON.parse(data.slice(6))
|
|
334
|
+
if (event.t === 'd' || event.t === 'md' || event.t === 'r') {
|
|
335
|
+
if (event.t === 'd') fullText += event.text || ''
|
|
336
|
+
else fullText = event.text || ''
|
|
337
|
+
}
|
|
338
|
+
} catch { /* ignore */ }
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
active,
|
|
342
|
+
loadHistory: () => mockSession.messages,
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
return fullText
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function extractJsonCommands(text: string): any[] {
|
|
349
|
+
const commands: any[] = []
|
|
350
|
+
// Match JSON blocks on their own lines
|
|
351
|
+
const regex = /^\s*(\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\})\s*$/gm
|
|
352
|
+
let match
|
|
353
|
+
while ((match = regex.exec(text)) !== null) {
|
|
354
|
+
try {
|
|
355
|
+
const parsed = JSON.parse(match[1])
|
|
356
|
+
if (parsed.delegate || parsed.memory_store || parsed.memory_read || parsed.done) {
|
|
357
|
+
commands.push(parsed)
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
// Not valid JSON, skip
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return commands
|
|
364
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP proxy for Playwright that intercepts browser_screenshot responses,
|
|
4
|
+
* saves images to the uploads directory, and tells Claude the image URL
|
|
5
|
+
* so it can reference it in its response.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from 'child_process'
|
|
8
|
+
import fs from 'fs'
|
|
9
|
+
import path from 'path'
|
|
10
|
+
import os from 'os'
|
|
11
|
+
|
|
12
|
+
const UPLOAD_DIR = process.env.SWARMCLAW_UPLOAD_DIR || path.join(os.tmpdir(), 'swarmclaw-uploads')
|
|
13
|
+
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
|
|
14
|
+
|
|
15
|
+
const child = spawn('npx', ['@playwright/mcp@latest'], {
|
|
16
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
// Forward stdin → child
|
|
20
|
+
process.stdin.on('data', (chunk) => child.stdin.write(chunk))
|
|
21
|
+
process.stdin.on('end', () => child.stdin.end())
|
|
22
|
+
|
|
23
|
+
// Parse MCP Content-Length framed messages from child stdout, intercept screenshots
|
|
24
|
+
let buf = ''
|
|
25
|
+
child.stdout.on('data', (chunk) => {
|
|
26
|
+
buf += chunk.toString()
|
|
27
|
+
while (true) {
|
|
28
|
+
const headerEnd = buf.indexOf('\r\n\r\n')
|
|
29
|
+
if (headerEnd === -1) break
|
|
30
|
+
const header = buf.slice(0, headerEnd)
|
|
31
|
+
const match = header.match(/Content-Length:\s*(\d+)/i)
|
|
32
|
+
if (!match) { buf = buf.slice(headerEnd + 4); continue }
|
|
33
|
+
const contentLength = parseInt(match[1])
|
|
34
|
+
const bodyStart = headerEnd + 4
|
|
35
|
+
if (buf.length < bodyStart + contentLength) break
|
|
36
|
+
const body = buf.slice(bodyStart, bodyStart + contentLength)
|
|
37
|
+
buf = buf.slice(bodyStart + contentLength)
|
|
38
|
+
|
|
39
|
+
let output
|
|
40
|
+
try {
|
|
41
|
+
const msg = JSON.parse(body)
|
|
42
|
+
if (msg.result?.content && Array.isArray(msg.result.content)) {
|
|
43
|
+
const newContent = []
|
|
44
|
+
for (const block of msg.result.content) {
|
|
45
|
+
if (block.type === 'image' && block.data) {
|
|
46
|
+
const filename = `screenshot-${Date.now()}.png`
|
|
47
|
+
fs.writeFileSync(path.join(UPLOAD_DIR, filename), Buffer.from(block.data, 'base64'))
|
|
48
|
+
newContent.push({
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: `Screenshot saved. Show it to the user with this markdown: `,
|
|
51
|
+
})
|
|
52
|
+
newContent.push(block) // keep image so Claude can see it
|
|
53
|
+
} else {
|
|
54
|
+
newContent.push(block)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
msg.result.content = newContent
|
|
58
|
+
}
|
|
59
|
+
output = JSON.stringify(msg)
|
|
60
|
+
} catch {
|
|
61
|
+
output = body
|
|
62
|
+
}
|
|
63
|
+
const frame = `Content-Length: ${Buffer.byteLength(output)}\r\n\r\n${output}`
|
|
64
|
+
process.stdout.write(frame)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
child.stderr.on('data', (chunk) => process.stderr.write(chunk))
|
|
69
|
+
child.on('close', (code) => process.exit(code || 0))
|
|
70
|
+
child.on('error', (err) => { process.stderr.write(`Proxy error: ${err.message}\n`); process.exit(1) })
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { createRequire } from 'module'
|
|
4
|
+
import type { Plugin, PluginHooks, PluginMeta } from '@/types'
|
|
5
|
+
|
|
6
|
+
import { DATA_DIR } from './data-dir'
|
|
7
|
+
|
|
8
|
+
const PLUGINS_DIR = path.join(DATA_DIR, 'plugins')
|
|
9
|
+
const PLUGINS_CONFIG = path.join(DATA_DIR, 'plugins.json')
|
|
10
|
+
|
|
11
|
+
// OpenClaw plugin format: { name, version, activate(ctx), deactivate() }
|
|
12
|
+
interface OpenClawPlugin {
|
|
13
|
+
name: string
|
|
14
|
+
version?: string
|
|
15
|
+
activate: (ctx: Record<string, (fn: (...args: any[]) => any) => void>) => void
|
|
16
|
+
deactivate?: () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Normalize a module export into SwarmClaw's Plugin interface.
|
|
21
|
+
* Supports both SwarmClaw format ({ name, hooks }) and OpenClaw format
|
|
22
|
+
* ({ name, activate(ctx) }) where activate receives event hook registrars.
|
|
23
|
+
*/
|
|
24
|
+
function normalizePlugin(mod: any): Plugin | null {
|
|
25
|
+
const raw = mod.default || mod
|
|
26
|
+
|
|
27
|
+
// SwarmClaw native format
|
|
28
|
+
if (raw.name && raw.hooks) {
|
|
29
|
+
return raw as Plugin
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// OpenClaw format: { name, activate(ctx), deactivate() }
|
|
33
|
+
if (raw.name && typeof raw.activate === 'function') {
|
|
34
|
+
const oc = raw as OpenClawPlugin
|
|
35
|
+
const hooks: PluginHooks = {}
|
|
36
|
+
|
|
37
|
+
// OpenClaw's activate receives an object of hook registrars.
|
|
38
|
+
// Map OpenClaw lifecycle names to SwarmClaw hook names.
|
|
39
|
+
const registrar: Record<string, (fn: (...args: any[]) => any) => void> = {
|
|
40
|
+
onAgentStart: (fn) => { hooks.beforeAgentStart = fn },
|
|
41
|
+
onAgentComplete: (fn) => { hooks.afterAgentComplete = fn },
|
|
42
|
+
onToolCall: (fn) => { hooks.beforeToolExec = fn },
|
|
43
|
+
onToolResult: (fn) => { hooks.afterToolExec = fn },
|
|
44
|
+
onMessage: (fn) => { hooks.onMessage = fn },
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
oc.activate(registrar)
|
|
49
|
+
} catch (err: any) {
|
|
50
|
+
console.error(`[plugins] OpenClaw activate() failed for ${oc.name}:`, err.message)
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
name: oc.name,
|
|
56
|
+
description: `OpenClaw plugin (v${oc.version || '0.0.0'})`,
|
|
57
|
+
hooks,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Ensure directories exist
|
|
65
|
+
if (!fs.existsSync(PLUGINS_DIR)) fs.mkdirSync(PLUGINS_DIR, { recursive: true })
|
|
66
|
+
if (!fs.existsSync(PLUGINS_CONFIG)) fs.writeFileSync(PLUGINS_CONFIG, '{}')
|
|
67
|
+
|
|
68
|
+
// Use createRequire to avoid Turbopack static analysis of require()
|
|
69
|
+
const dynamicRequire = createRequire(import.meta.url || __filename)
|
|
70
|
+
|
|
71
|
+
interface LoadedPlugin {
|
|
72
|
+
meta: PluginMeta
|
|
73
|
+
hooks: PluginHooks
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class PluginManager {
|
|
77
|
+
private plugins: LoadedPlugin[] = []
|
|
78
|
+
private loaded = false
|
|
79
|
+
|
|
80
|
+
load() {
|
|
81
|
+
if (this.loaded) return
|
|
82
|
+
this.plugins = []
|
|
83
|
+
|
|
84
|
+
const config = this.loadConfig()
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const files = fs.readdirSync(PLUGINS_DIR).filter(
|
|
88
|
+
(f) => f.endsWith('.js') || f.endsWith('.mjs'),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
try {
|
|
93
|
+
const fullPath = path.join(PLUGINS_DIR, file)
|
|
94
|
+
// Clear require cache to allow reloads
|
|
95
|
+
delete dynamicRequire.cache[fullPath]
|
|
96
|
+
const mod = dynamicRequire(fullPath)
|
|
97
|
+
const plugin = normalizePlugin(mod)
|
|
98
|
+
|
|
99
|
+
if (!plugin) {
|
|
100
|
+
console.warn(`[plugins] Skipping ${file}: unrecognized plugin format`)
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const isEnabled = config[file]?.enabled !== false // enabled by default
|
|
105
|
+
|
|
106
|
+
if (isEnabled) {
|
|
107
|
+
this.plugins.push({
|
|
108
|
+
meta: {
|
|
109
|
+
name: plugin.name,
|
|
110
|
+
description: plugin.description,
|
|
111
|
+
filename: file,
|
|
112
|
+
enabled: true,
|
|
113
|
+
},
|
|
114
|
+
hooks: plugin.hooks,
|
|
115
|
+
})
|
|
116
|
+
console.log(`[plugins] Loaded: ${plugin.name} (${file})`)
|
|
117
|
+
}
|
|
118
|
+
} catch (err: any) {
|
|
119
|
+
console.error(`[plugins] Failed to load ${file}:`, err.message)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// plugins dir doesn't exist or can't be read
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.loaded = true
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async runHook<K extends keyof PluginHooks>(
|
|
130
|
+
hookName: K,
|
|
131
|
+
ctx: Parameters<NonNullable<PluginHooks[K]>>[0],
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
this.load()
|
|
134
|
+
for (const plugin of this.plugins) {
|
|
135
|
+
const hook = plugin.hooks[hookName]
|
|
136
|
+
if (hook) {
|
|
137
|
+
try {
|
|
138
|
+
await (hook as any)(ctx)
|
|
139
|
+
} catch (err: any) {
|
|
140
|
+
console.error(`[plugins] Error in ${plugin.meta.name}.${hookName}:`, err.message)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
listPlugins(): PluginMeta[] {
|
|
147
|
+
this.load()
|
|
148
|
+
const config = this.loadConfig()
|
|
149
|
+
|
|
150
|
+
// Include both loaded and disabled plugins
|
|
151
|
+
const metas: PluginMeta[] = this.plugins.map((p) => p.meta)
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const files = fs.readdirSync(PLUGINS_DIR).filter(
|
|
155
|
+
(f) => f.endsWith('.js') || f.endsWith('.mjs'),
|
|
156
|
+
)
|
|
157
|
+
for (const file of files) {
|
|
158
|
+
if (!metas.find((m) => m.filename === file)) {
|
|
159
|
+
metas.push({
|
|
160
|
+
name: file.replace(/\.(js|mjs)$/, ''),
|
|
161
|
+
filename: file,
|
|
162
|
+
enabled: config[file]?.enabled !== false,
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch { /* ignore */ }
|
|
167
|
+
|
|
168
|
+
return metas
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setEnabled(filename: string, enabled: boolean) {
|
|
172
|
+
const config = this.loadConfig()
|
|
173
|
+
config[filename] = { ...config[filename], enabled }
|
|
174
|
+
fs.writeFileSync(PLUGINS_CONFIG, JSON.stringify(config, null, 2))
|
|
175
|
+
// Force reload on next hook call
|
|
176
|
+
this.loaded = false
|
|
177
|
+
this.plugins = []
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async installPlugin(url: string, filename: string): Promise<{ ok: boolean; error?: string }> {
|
|
181
|
+
if (!url.startsWith('https://')) {
|
|
182
|
+
return { ok: false, error: 'URL must be HTTPS' }
|
|
183
|
+
}
|
|
184
|
+
const sanitized = path.basename(filename)
|
|
185
|
+
if (sanitized !== filename || !filename.endsWith('.js')) {
|
|
186
|
+
return { ok: false, error: 'Invalid filename' }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const res = await fetch(url)
|
|
191
|
+
if (!res.ok) throw new Error(`Download failed: ${res.status}`)
|
|
192
|
+
const code = await res.text()
|
|
193
|
+
|
|
194
|
+
if (!fs.existsSync(PLUGINS_DIR)) {
|
|
195
|
+
fs.mkdirSync(PLUGINS_DIR, { recursive: true })
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fs.writeFileSync(path.join(PLUGINS_DIR, sanitized), code, 'utf8')
|
|
199
|
+
this.reload()
|
|
200
|
+
return { ok: true }
|
|
201
|
+
} catch (err: any) {
|
|
202
|
+
return { ok: false, error: err.message }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private loadConfig(): Record<string, { enabled: boolean }> {
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(fs.readFileSync(PLUGINS_CONFIG, 'utf8'))
|
|
209
|
+
} catch {
|
|
210
|
+
return {}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
reload() {
|
|
215
|
+
this.loaded = false
|
|
216
|
+
this.plugins = []
|
|
217
|
+
this.load()
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let _manager: PluginManager | null = null
|
|
222
|
+
|
|
223
|
+
export function getPluginManager(): PluginManager {
|
|
224
|
+
if (!_manager) {
|
|
225
|
+
_manager = new PluginManager()
|
|
226
|
+
_manager.load()
|
|
227
|
+
}
|
|
228
|
+
return _manager
|
|
229
|
+
}
|