@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,183 @@
|
|
|
1
|
+
import { spawn, execSync } from 'child_process'
|
|
2
|
+
import type { ChildProcess } from 'child_process'
|
|
3
|
+
import type { Connector } from '@/types'
|
|
4
|
+
import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
|
|
5
|
+
import { isNoMessage } from './manager'
|
|
6
|
+
|
|
7
|
+
const signal: PlatformConnector = {
|
|
8
|
+
async start(connector, _botToken, onMessage): Promise<ConnectorInstance> {
|
|
9
|
+
const phoneNumber = connector.config.phoneNumber
|
|
10
|
+
if (!phoneNumber) throw new Error('Missing phoneNumber in connector config')
|
|
11
|
+
|
|
12
|
+
const cliPath = connector.config.signalCliPath || 'signal-cli'
|
|
13
|
+
const mode = connector.config.signalCliMode || 'stdio'
|
|
14
|
+
const httpUrl = connector.config.signalCliHttpUrl || 'http://localhost:8080'
|
|
15
|
+
|
|
16
|
+
let stopped = false
|
|
17
|
+
let daemonProc: ChildProcess | null = null
|
|
18
|
+
let pollTimer: ReturnType<typeof setInterval> | null = null
|
|
19
|
+
|
|
20
|
+
if (mode === 'stdio') {
|
|
21
|
+
// Spawn signal-cli in daemon mode with JSON output
|
|
22
|
+
daemonProc = spawn(cliPath, ['-u', phoneNumber, 'daemon', '--json'], {
|
|
23
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
let buffer = ''
|
|
27
|
+
|
|
28
|
+
daemonProc.stdout?.on('data', (chunk: Buffer) => {
|
|
29
|
+
buffer += chunk.toString()
|
|
30
|
+
const lines = buffer.split('\n')
|
|
31
|
+
buffer = lines.pop() || ''
|
|
32
|
+
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
if (!line.trim()) continue
|
|
35
|
+
try {
|
|
36
|
+
const event = JSON.parse(line)
|
|
37
|
+
handleSignalEvent(event, connector, onMessage)
|
|
38
|
+
} catch {
|
|
39
|
+
// Not valid JSON, skip
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
daemonProc.stderr?.on('data', (chunk: Buffer) => {
|
|
45
|
+
const msg = chunk.toString().trim()
|
|
46
|
+
if (msg) console.error(`[signal] stderr: ${msg}`)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
daemonProc.on('exit', (code) => {
|
|
50
|
+
if (!stopped) {
|
|
51
|
+
console.error(`[signal] daemon exited unexpectedly with code ${code}`)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
console.log(`[signal] Daemon started in stdio mode for ${phoneNumber}`)
|
|
56
|
+
} else if (mode === 'http') {
|
|
57
|
+
// Poll the signal-cli REST API for incoming messages
|
|
58
|
+
const pollInterval = 2000
|
|
59
|
+
|
|
60
|
+
const poll = async () => {
|
|
61
|
+
if (stopped) return
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(`${httpUrl}/v1/receive/${phoneNumber}`)
|
|
64
|
+
if (!res.ok) return
|
|
65
|
+
const messages = await res.json()
|
|
66
|
+
if (Array.isArray(messages)) {
|
|
67
|
+
for (const event of messages) {
|
|
68
|
+
handleSignalEvent(event, connector, onMessage)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Silently retry on connection errors
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pollTimer = setInterval(poll, pollInterval)
|
|
77
|
+
console.log(`[signal] Polling ${httpUrl} for ${phoneNumber} every ${pollInterval}ms`)
|
|
78
|
+
} else {
|
|
79
|
+
throw new Error(`Unknown signalCliMode: ${mode}. Use 'stdio' or 'http'.`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
connector,
|
|
84
|
+
async sendMessage(channelId, text) {
|
|
85
|
+
if (stopped) throw new Error('Connector is stopped')
|
|
86
|
+
|
|
87
|
+
if (mode === 'http') {
|
|
88
|
+
const res = await fetch(`${httpUrl}/v2/send`, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: { 'Content-Type': 'application/json' },
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
message: text,
|
|
93
|
+
number: phoneNumber,
|
|
94
|
+
recipients: [channelId],
|
|
95
|
+
}),
|
|
96
|
+
})
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
throw new Error(`Signal HTTP send failed: ${res.status} ${res.statusText}`)
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// Use signal-cli send command
|
|
102
|
+
try {
|
|
103
|
+
execSync(
|
|
104
|
+
`${cliPath} -u ${phoneNumber} send -m ${JSON.stringify(text)} ${channelId}`,
|
|
105
|
+
{ timeout: 15_000 },
|
|
106
|
+
)
|
|
107
|
+
} catch (err: any) {
|
|
108
|
+
throw new Error(`Signal send failed: ${err.message}`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
async stop() {
|
|
113
|
+
stopped = true
|
|
114
|
+
if (pollTimer) {
|
|
115
|
+
clearInterval(pollTimer)
|
|
116
|
+
pollTimer = null
|
|
117
|
+
}
|
|
118
|
+
if (daemonProc) {
|
|
119
|
+
daemonProc.kill('SIGTERM')
|
|
120
|
+
daemonProc = null
|
|
121
|
+
}
|
|
122
|
+
console.log(`[signal] Connector stopped for ${phoneNumber}`)
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Parse a signal-cli JSON event and route it as an inbound message */
|
|
129
|
+
export async function handleSignalEvent(
|
|
130
|
+
event: any,
|
|
131
|
+
connector: Connector,
|
|
132
|
+
onMessage: (msg: InboundMessage) => Promise<string>,
|
|
133
|
+
) {
|
|
134
|
+
// signal-cli JSON output structure varies; handle the common envelope format
|
|
135
|
+
const envelope = event.envelope || event
|
|
136
|
+
const dataMessage = envelope.dataMessage
|
|
137
|
+
if (!dataMessage?.message && !dataMessage?.body) return
|
|
138
|
+
|
|
139
|
+
const sender = envelope.source || envelope.sourceNumber || ''
|
|
140
|
+
const text = dataMessage.message || dataMessage.body || ''
|
|
141
|
+
const groupId = dataMessage.groupInfo?.groupId || null
|
|
142
|
+
|
|
143
|
+
const inbound: InboundMessage = {
|
|
144
|
+
platform: 'signal',
|
|
145
|
+
channelId: groupId || sender,
|
|
146
|
+
channelName: groupId ? `group:${groupId}` : sender,
|
|
147
|
+
senderId: sender,
|
|
148
|
+
senderName: envelope.sourceName || sender,
|
|
149
|
+
text,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const response = await onMessage(inbound)
|
|
154
|
+
if (isNoMessage(response)) return
|
|
155
|
+
|
|
156
|
+
// Send reply back
|
|
157
|
+
const cliPath = connector.config.signalCliPath || 'signal-cli'
|
|
158
|
+
const phoneNumber = connector.config.phoneNumber
|
|
159
|
+
const mode = connector.config.signalCliMode || 'stdio'
|
|
160
|
+
const httpUrl = connector.config.signalCliHttpUrl || 'http://localhost:8080'
|
|
161
|
+
|
|
162
|
+
if (mode === 'http') {
|
|
163
|
+
await fetch(`${httpUrl}/v2/send`, {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: { 'Content-Type': 'application/json' },
|
|
166
|
+
body: JSON.stringify({
|
|
167
|
+
message: response,
|
|
168
|
+
number: phoneNumber,
|
|
169
|
+
recipients: [inbound.channelId],
|
|
170
|
+
}),
|
|
171
|
+
})
|
|
172
|
+
} else {
|
|
173
|
+
execSync(
|
|
174
|
+
`${cliPath} -u ${phoneNumber} send -m ${JSON.stringify(response)} ${inbound.channelId}`,
|
|
175
|
+
{ timeout: 15_000 },
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
} catch (err: any) {
|
|
179
|
+
console.error(`[signal] Error handling message:`, err.message)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export default signal
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { App, LogLevel } from '@slack/bolt'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import type { Connector } from '@/types'
|
|
5
|
+
import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
|
|
6
|
+
import { downloadInboundMediaToUpload, inferInboundMediaType, mimeFromPath, isImageMime } from './media'
|
|
7
|
+
import { isNoMessage } from './manager'
|
|
8
|
+
|
|
9
|
+
const slack: PlatformConnector = {
|
|
10
|
+
async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
|
|
11
|
+
const appToken = connector.config.appToken || ''
|
|
12
|
+
const signingSecret = connector.config.signingSecret || 'not-used-in-socket-mode'
|
|
13
|
+
|
|
14
|
+
// Socket Mode requires an app-level token (xapp-...) — without it, Bolt starts an HTTP server
|
|
15
|
+
if (!appToken) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'App-Level Token (xapp-...) is required. Enable Socket Mode in your Slack app settings ' +
|
|
18
|
+
'and generate an App-Level Token under Basic Information > App-Level Tokens.'
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!appToken.startsWith('xapp-')) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Invalid App-Level Token — must start with "xapp-" (got "${appToken.slice(0, 5)}..."). ` +
|
|
25
|
+
'The App-Level Token is different from the Bot Token (xoxb-). ' +
|
|
26
|
+
'Find it under Basic Information > App-Level Tokens in your Slack app settings.'
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Validate the bot token format and auth
|
|
31
|
+
if (!botToken.startsWith('xoxb-')) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Invalid Bot Token — must start with "xoxb-" (got "${botToken.slice(0, 5)}..."). ` +
|
|
34
|
+
'Find it under OAuth & Permissions > Bot User OAuth Token.'
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { WebClient } = await import('@slack/web-api')
|
|
39
|
+
const testClient = new WebClient(botToken)
|
|
40
|
+
let botUserId: string | undefined
|
|
41
|
+
try {
|
|
42
|
+
const auth = await testClient.auth.test()
|
|
43
|
+
if (!auth.user_id || !auth.team) {
|
|
44
|
+
throw new Error('Auth test returned empty — the bot token may be revoked or the app uninstalled')
|
|
45
|
+
}
|
|
46
|
+
botUserId = auth.user_id as string
|
|
47
|
+
console.log(`[slack] Authenticated as @${auth.user} in workspace "${auth.team}"`)
|
|
48
|
+
} catch (err: any) {
|
|
49
|
+
const hint = err.code === 'slack_webapi_platform_error'
|
|
50
|
+
? '. Check that your Bot Token (xoxb-...) is correct and the app is installed to the workspace.'
|
|
51
|
+
: ''
|
|
52
|
+
throw new Error(`Slack auth failed: ${err.message}${hint}`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const app = new App({
|
|
56
|
+
token: botToken,
|
|
57
|
+
appToken,
|
|
58
|
+
signingSecret,
|
|
59
|
+
socketMode: true,
|
|
60
|
+
logLevel: LogLevel.WARN,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Catch global errors so they don't become unhandled rejections
|
|
64
|
+
app.error(async (error) => {
|
|
65
|
+
console.error(`[slack] App error:`, error)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// Optional: restrict to specific channels
|
|
69
|
+
const allowedChannels = connector.config.channelIds
|
|
70
|
+
? connector.config.channelIds.split(',').map((s) => s.trim()).filter(Boolean)
|
|
71
|
+
: null
|
|
72
|
+
|
|
73
|
+
// Handle messages
|
|
74
|
+
app.message(async ({ message, say, client }) => {
|
|
75
|
+
// Only handle user messages (not bot messages or own messages)
|
|
76
|
+
if (!('text' in message) || ('bot_id' in message)) return
|
|
77
|
+
const msg = message as any
|
|
78
|
+
if (botUserId && msg.user === botUserId) return
|
|
79
|
+
|
|
80
|
+
const channelId = msg.channel
|
|
81
|
+
if (allowedChannels && !allowedChannels.includes(channelId)) return
|
|
82
|
+
|
|
83
|
+
console.log(`[slack] Message in ${channelId} from ${msg.user}: ${(msg.text || '').slice(0, 80)}`)
|
|
84
|
+
|
|
85
|
+
// Get user info for display name
|
|
86
|
+
let senderName = msg.user || 'unknown'
|
|
87
|
+
try {
|
|
88
|
+
const userInfo = await client.users.info({ user: msg.user })
|
|
89
|
+
senderName = userInfo.user?.real_name || userInfo.user?.name || senderName
|
|
90
|
+
} catch { /* use ID as fallback */ }
|
|
91
|
+
|
|
92
|
+
// Get channel name
|
|
93
|
+
let channelName = channelId
|
|
94
|
+
try {
|
|
95
|
+
const channelInfo = await client.conversations.info({ channel: channelId })
|
|
96
|
+
channelName = (channelInfo.channel as any)?.name || channelId
|
|
97
|
+
} catch { /* use ID as fallback */ }
|
|
98
|
+
|
|
99
|
+
const media: NonNullable<InboundMessage['media']> = []
|
|
100
|
+
if (Array.isArray(msg.files)) {
|
|
101
|
+
for (const f of msg.files as any[]) {
|
|
102
|
+
const mediaType = inferInboundMediaType(f?.mimetype, f?.name, 'document')
|
|
103
|
+
const sourceUrl = f?.url_private_download || f?.url_private || f?.permalink_public || f?.permalink
|
|
104
|
+
if (typeof sourceUrl === 'string' && /^https?:\/\//i.test(sourceUrl)) {
|
|
105
|
+
try {
|
|
106
|
+
const stored = await downloadInboundMediaToUpload({
|
|
107
|
+
connectorId: connector.id,
|
|
108
|
+
mediaType,
|
|
109
|
+
url: sourceUrl,
|
|
110
|
+
headers: { Authorization: `Bearer ${botToken}` },
|
|
111
|
+
fileName: f?.name || undefined,
|
|
112
|
+
mimeType: f?.mimetype || undefined,
|
|
113
|
+
})
|
|
114
|
+
if (stored) {
|
|
115
|
+
media.push(stored)
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
} catch (err: any) {
|
|
119
|
+
console.warn(`[slack] Media download failed (${f?.name || 'file'}):`, err?.message || String(err))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
media.push({
|
|
123
|
+
type: mediaType,
|
|
124
|
+
fileName: f?.name || undefined,
|
|
125
|
+
mimeType: f?.mimetype || undefined,
|
|
126
|
+
sizeBytes: typeof f?.size === 'number' ? f.size : undefined,
|
|
127
|
+
url: typeof sourceUrl === 'string' ? sourceUrl : undefined,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const inbound: InboundMessage = {
|
|
133
|
+
platform: 'slack',
|
|
134
|
+
channelId,
|
|
135
|
+
channelName,
|
|
136
|
+
senderId: msg.user,
|
|
137
|
+
senderName,
|
|
138
|
+
text: msg.text || (media.length > 0 ? '(media message)' : ''),
|
|
139
|
+
imageUrl: media.find((m) => m.type === 'image')?.url,
|
|
140
|
+
media,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const response = await onMessage(inbound)
|
|
145
|
+
|
|
146
|
+
if (isNoMessage(response)) return
|
|
147
|
+
|
|
148
|
+
// Slack has a 4000 char limit for messages
|
|
149
|
+
if (response.length <= 4000) {
|
|
150
|
+
await say(response)
|
|
151
|
+
} else {
|
|
152
|
+
const chunks = response.match(/[\s\S]{1,3990}/g) || [response]
|
|
153
|
+
for (const chunk of chunks) {
|
|
154
|
+
await say(chunk)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (err: any) {
|
|
158
|
+
console.error(`[slack] Error handling message:`, err.message)
|
|
159
|
+
try {
|
|
160
|
+
await say('Sorry, I encountered an error processing your message.')
|
|
161
|
+
} catch { /* ignore */ }
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Handle @mentions
|
|
166
|
+
app.event('app_mention', async ({ event, say, client }) => {
|
|
167
|
+
if (allowedChannels && !allowedChannels.includes(event.channel)) return
|
|
168
|
+
|
|
169
|
+
let senderName = event.user || 'unknown'
|
|
170
|
+
try {
|
|
171
|
+
const userInfo = await client.users.info({ user: event.user! })
|
|
172
|
+
senderName = userInfo.user?.real_name || userInfo.user?.name || senderName
|
|
173
|
+
} catch { /* use ID */ }
|
|
174
|
+
|
|
175
|
+
const inbound: InboundMessage = {
|
|
176
|
+
platform: 'slack',
|
|
177
|
+
channelId: event.channel,
|
|
178
|
+
channelName: event.channel,
|
|
179
|
+
senderId: event.user || 'unknown',
|
|
180
|
+
senderName,
|
|
181
|
+
text: event.text.replace(/<@[^>]+>/g, '').trim(), // Strip @mentions
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const response = await onMessage(inbound)
|
|
186
|
+
if (isNoMessage(response)) return
|
|
187
|
+
await say(response)
|
|
188
|
+
} catch (err: any) {
|
|
189
|
+
console.error(`[slack] Error handling mention:`, err.message)
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
await app.start()
|
|
194
|
+
console.log(`[slack] Bot connected (socket mode)`)
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
connector,
|
|
198
|
+
async sendMessage(channelId, text, options) {
|
|
199
|
+
const webClient = app.client
|
|
200
|
+
|
|
201
|
+
// File upload (local path or URL)
|
|
202
|
+
const hasMedia = options?.mediaPath || options?.imageUrl || options?.fileUrl
|
|
203
|
+
if (hasMedia) {
|
|
204
|
+
let fileContent: Buffer | undefined
|
|
205
|
+
let fileUrl: string | undefined
|
|
206
|
+
let fileName = options?.fileName || 'attachment'
|
|
207
|
+
|
|
208
|
+
if (options?.mediaPath) {
|
|
209
|
+
if (!fs.existsSync(options.mediaPath)) throw new Error(`File not found: ${options.mediaPath}`)
|
|
210
|
+
fileContent = fs.readFileSync(options.mediaPath)
|
|
211
|
+
fileName = options.fileName || path.basename(options.mediaPath)
|
|
212
|
+
} else {
|
|
213
|
+
fileUrl = options?.imageUrl || options?.fileUrl
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (fileContent) {
|
|
217
|
+
const result = await webClient.filesUploadV2({
|
|
218
|
+
channel_id: channelId,
|
|
219
|
+
file: fileContent,
|
|
220
|
+
filename: fileName,
|
|
221
|
+
initial_comment: options?.caption || text || undefined,
|
|
222
|
+
})
|
|
223
|
+
return { messageId: (result as any)?.files?.[0]?.id }
|
|
224
|
+
} else if (fileUrl) {
|
|
225
|
+
// Send URL as message with unfurl
|
|
226
|
+
const msg = await webClient.chat.postMessage({
|
|
227
|
+
channel: channelId,
|
|
228
|
+
text: `${options?.caption || text || ''}\n${fileUrl}`.trim(),
|
|
229
|
+
unfurl_links: true,
|
|
230
|
+
unfurl_media: true,
|
|
231
|
+
})
|
|
232
|
+
return { messageId: msg.ts || undefined }
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Text only
|
|
237
|
+
const payload = text || options?.caption || ''
|
|
238
|
+
if (payload.length <= 4000) {
|
|
239
|
+
const msg = await webClient.chat.postMessage({ channel: channelId, text: payload })
|
|
240
|
+
return { messageId: msg.ts || undefined }
|
|
241
|
+
}
|
|
242
|
+
const chunks = payload.match(/[\s\S]{1,3990}/g) || [payload]
|
|
243
|
+
let lastTs: string | undefined
|
|
244
|
+
for (const chunk of chunks) {
|
|
245
|
+
const msg = await webClient.chat.postMessage({ channel: channelId, text: chunk })
|
|
246
|
+
lastTs = msg.ts || undefined
|
|
247
|
+
}
|
|
248
|
+
return { messageId: lastTs }
|
|
249
|
+
},
|
|
250
|
+
async stop() {
|
|
251
|
+
await app.stop()
|
|
252
|
+
console.log(`[slack] Bot disconnected`)
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export default slack
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
|
|
2
|
+
import { isNoMessage } from './manager'
|
|
3
|
+
|
|
4
|
+
const teams: PlatformConnector = {
|
|
5
|
+
async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
|
|
6
|
+
const pkg = 'botbuilder'
|
|
7
|
+
const { BotFrameworkAdapter, TurnContext } = await import(/* webpackIgnore: true */ pkg)
|
|
8
|
+
|
|
9
|
+
const appId = connector.config.appId
|
|
10
|
+
if (!appId) throw new Error('Missing appId in connector config')
|
|
11
|
+
|
|
12
|
+
const adapter = new BotFrameworkAdapter({
|
|
13
|
+
appId,
|
|
14
|
+
appPassword: botToken,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
adapter.onTurnError = async (_context: unknown, error: Error) => {
|
|
18
|
+
console.error(`[teams] Turn error:`, error.message)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Store conversation references for proactive messaging
|
|
22
|
+
const conversationReferences = new Map<string, any>()
|
|
23
|
+
let stopped = false
|
|
24
|
+
|
|
25
|
+
// Process incoming activities — called from the webhook endpoint
|
|
26
|
+
// POST /api/connectors/[id]/webhook should pipe req/res through this
|
|
27
|
+
const processActivity = async (req: any, res: any) => {
|
|
28
|
+
if (stopped) return
|
|
29
|
+
await adapter.processActivity(req, res, async (context: any) => {
|
|
30
|
+
if (context.activity.type !== 'message') return
|
|
31
|
+
if (!context.activity.text) return
|
|
32
|
+
|
|
33
|
+
// Save conversation reference for proactive messaging
|
|
34
|
+
const ref = TurnContext.getConversationReference(context.activity)
|
|
35
|
+
const convId = context.activity.conversation?.id || ''
|
|
36
|
+
conversationReferences.set(convId, ref)
|
|
37
|
+
|
|
38
|
+
const inbound: InboundMessage = {
|
|
39
|
+
platform: 'teams',
|
|
40
|
+
channelId: convId,
|
|
41
|
+
channelName: context.activity.conversation?.name || convId,
|
|
42
|
+
senderId: context.activity.from?.id || '',
|
|
43
|
+
senderName: context.activity.from?.name || 'Unknown',
|
|
44
|
+
text: context.activity.text || '',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const response = await onMessage(inbound)
|
|
49
|
+
if (isNoMessage(response)) return
|
|
50
|
+
await context.sendActivity(response)
|
|
51
|
+
} catch (err: any) {
|
|
52
|
+
console.error(`[teams] Error handling message:`, err.message)
|
|
53
|
+
try {
|
|
54
|
+
await context.sendActivity('Sorry, I encountered an error processing your message.')
|
|
55
|
+
} catch { /* ignore */ }
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Store processActivity on globalThis so the webhook route can access it
|
|
61
|
+
const handlerKey = `__swarmclaw_teams_handler_${connector.id}__`
|
|
62
|
+
;(globalThis as any)[handlerKey] = processActivity
|
|
63
|
+
|
|
64
|
+
console.log(`[teams] Bot registered (appId: ${appId})`)
|
|
65
|
+
console.log(`[teams] Configure your bot's messaging endpoint to POST to /api/connectors/${connector.id}/webhook`)
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
connector,
|
|
69
|
+
async sendMessage(channelId, text) {
|
|
70
|
+
if (stopped) throw new Error('Connector is stopped')
|
|
71
|
+
|
|
72
|
+
const ref = conversationReferences.get(channelId)
|
|
73
|
+
if (!ref) {
|
|
74
|
+
throw new Error(`No conversation reference found for ${channelId}. The bot must receive a message first.`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let messageId: string | undefined
|
|
78
|
+
await adapter.continueConversation(ref, async (context: any) => {
|
|
79
|
+
const sent = await context.sendActivity(text)
|
|
80
|
+
messageId = sent?.id
|
|
81
|
+
})
|
|
82
|
+
return { messageId }
|
|
83
|
+
},
|
|
84
|
+
async stop() {
|
|
85
|
+
stopped = true
|
|
86
|
+
delete (globalThis as any)[handlerKey]
|
|
87
|
+
conversationReferences.clear()
|
|
88
|
+
console.log(`[teams] Bot disconnected`)
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default teams
|