@swarmclawai/swarmclaw 0.7.7 → 0.8.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 +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
export type ChatArtifactKind = 'image' | 'pdf' | 'markdown' | 'file' | 'site'
|
|
2
|
+
|
|
3
|
+
export interface ChatArtifactItem {
|
|
4
|
+
label: string
|
|
5
|
+
href: string
|
|
6
|
+
kind: ChatArtifactKind
|
|
7
|
+
filename: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ChatArtifactSection {
|
|
11
|
+
title: string
|
|
12
|
+
items: ChatArtifactItem[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ChatArtifactSummary {
|
|
16
|
+
title: string | null
|
|
17
|
+
intro: string[]
|
|
18
|
+
sections: ChatArtifactSection[]
|
|
19
|
+
liveSitesTitle: string | null
|
|
20
|
+
liveSites: ChatArtifactItem[]
|
|
21
|
+
counts: {
|
|
22
|
+
images: number
|
|
23
|
+
pdfs: number
|
|
24
|
+
markdown: number
|
|
25
|
+
files: number
|
|
26
|
+
sites: number
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TITLE_RE = /^##+\s+(.+?)\s*$/
|
|
31
|
+
const SECTION_RE = /^###\s+(.+?)\s*$/
|
|
32
|
+
const LINK_BULLET_RE = /^-\s+\[([^\]]+)\]\(([^)]+)\)\s*$/
|
|
33
|
+
const BOLD_URL_BULLET_RE = /^-\s+\*\*([^*]+)\*\*:\s+(https?:\/\/\S+)\s*$/
|
|
34
|
+
|
|
35
|
+
function stripMarkdown(text: string): string {
|
|
36
|
+
return text
|
|
37
|
+
.replace(/[*_`#]/g, '')
|
|
38
|
+
.replace(/\[(.*?)\]\([^)]+\)/g, '$1')
|
|
39
|
+
.trim()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function inferArtifactKind(href: string): ChatArtifactKind {
|
|
43
|
+
const normalized = href.trim().toLowerCase()
|
|
44
|
+
if (/^https?:\/\/localhost:\d+/.test(normalized)) return 'site'
|
|
45
|
+
if (/\.(png|jpe?g|gif|webp|svg|avif)(?:[?#]|$)/.test(normalized)) return 'image'
|
|
46
|
+
if (/\.pdf(?:[?#]|$)/.test(normalized)) return 'pdf'
|
|
47
|
+
if (/\.(md|markdown)(?:[?#]|$)/.test(normalized)) return 'markdown'
|
|
48
|
+
return 'file'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildArtifactItem(label: string, href: string): ChatArtifactItem {
|
|
52
|
+
const filename = href.split('/').pop()?.split('?')[0] || href
|
|
53
|
+
return {
|
|
54
|
+
label: stripMarkdown(label),
|
|
55
|
+
href: href.trim(),
|
|
56
|
+
kind: inferArtifactKind(href),
|
|
57
|
+
filename,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function pushSection(sections: ChatArtifactSection[], current: ChatArtifactSection | null) {
|
|
62
|
+
if (!current || current.items.length === 0) return
|
|
63
|
+
sections.push(current)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function countItems(sections: ChatArtifactSection[], liveSites: ChatArtifactItem[]) {
|
|
67
|
+
const counts = {
|
|
68
|
+
images: 0,
|
|
69
|
+
pdfs: 0,
|
|
70
|
+
markdown: 0,
|
|
71
|
+
files: 0,
|
|
72
|
+
sites: liveSites.length,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const item of sections.flatMap((section) => section.items)) {
|
|
76
|
+
if (item.kind === 'image') counts.images += 1
|
|
77
|
+
else if (item.kind === 'pdf') counts.pdfs += 1
|
|
78
|
+
else if (item.kind === 'markdown') counts.markdown += 1
|
|
79
|
+
else if (item.kind === 'site') counts.sites += 1
|
|
80
|
+
else counts.files += 1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return counts
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function parseChatArtifactSummary(markdown: string): ChatArtifactSummary | null {
|
|
87
|
+
const text = markdown.trim()
|
|
88
|
+
if (!text) return null
|
|
89
|
+
|
|
90
|
+
const lines = text.split(/\r?\n/)
|
|
91
|
+
let title: string | null = null
|
|
92
|
+
let currentSection: ChatArtifactSection | null = null
|
|
93
|
+
const sections: ChatArtifactSection[] = []
|
|
94
|
+
const intro: string[] = []
|
|
95
|
+
const liveSites: ChatArtifactItem[] = []
|
|
96
|
+
let liveSitesTitle: string | null = null
|
|
97
|
+
let seenSection = false
|
|
98
|
+
|
|
99
|
+
for (const rawLine of lines) {
|
|
100
|
+
const line = rawLine.trim()
|
|
101
|
+
if (!line) continue
|
|
102
|
+
|
|
103
|
+
const titleMatch = !seenSection ? line.match(TITLE_RE) : null
|
|
104
|
+
if (titleMatch) {
|
|
105
|
+
title = stripMarkdown(titleMatch[1])
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const sectionMatch = line.match(SECTION_RE)
|
|
110
|
+
if (sectionMatch) {
|
|
111
|
+
pushSection(sections, currentSection)
|
|
112
|
+
currentSection = { title: stripMarkdown(sectionMatch[1]), items: [] }
|
|
113
|
+
seenSection = true
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const linkMatch = line.match(LINK_BULLET_RE)
|
|
118
|
+
if (linkMatch) {
|
|
119
|
+
const item = buildArtifactItem(linkMatch[1], linkMatch[2])
|
|
120
|
+
if (currentSection) currentSection.items.push(item)
|
|
121
|
+
else if (item.kind === 'site') liveSites.push(item)
|
|
122
|
+
else intro.push(stripMarkdown(line))
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const liveSiteMatch = line.match(BOLD_URL_BULLET_RE)
|
|
127
|
+
if (liveSiteMatch) {
|
|
128
|
+
liveSites.push({
|
|
129
|
+
label: stripMarkdown(liveSiteMatch[1]),
|
|
130
|
+
href: liveSiteMatch[2],
|
|
131
|
+
kind: 'site',
|
|
132
|
+
filename: liveSiteMatch[2],
|
|
133
|
+
})
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!seenSection) {
|
|
138
|
+
intro.push(stripMarkdown(line))
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (currentSection?.items.length === 0 && !liveSites.length) {
|
|
143
|
+
liveSitesTitle = stripMarkdown(line)
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!liveSites.length) {
|
|
148
|
+
liveSitesTitle = stripMarkdown(line)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
pushSection(sections, currentSection)
|
|
153
|
+
|
|
154
|
+
const totalSectionItems = sections.reduce((sum, section) => sum + section.items.length, 0)
|
|
155
|
+
if (totalSectionItems < 3 || sections.length === 0) return null
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
title,
|
|
159
|
+
intro,
|
|
160
|
+
sections,
|
|
161
|
+
liveSitesTitle,
|
|
162
|
+
liveSites,
|
|
163
|
+
counts: countItems(sections, liveSites),
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { dedupeMessagesForDisplay, formatMessageTimestamp } from './chat-display'
|
|
5
|
+
import type { Message } from '@/types'
|
|
6
|
+
|
|
7
|
+
function baseMessage(overrides: Partial<Message> = {}): Message {
|
|
8
|
+
return {
|
|
9
|
+
role: 'user',
|
|
10
|
+
text: 'hello',
|
|
11
|
+
time: Date.now(),
|
|
12
|
+
...overrides,
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('dedupeMessagesForDisplay removes exact connector duplicates by message id', () => {
|
|
17
|
+
const first = baseMessage({
|
|
18
|
+
source: {
|
|
19
|
+
platform: 'whatsapp',
|
|
20
|
+
connectorId: 'conn-1',
|
|
21
|
+
connectorName: 'WhatsApp',
|
|
22
|
+
messageId: 'wamid-1',
|
|
23
|
+
senderName: 'Alice',
|
|
24
|
+
},
|
|
25
|
+
historyExcluded: true,
|
|
26
|
+
})
|
|
27
|
+
const duplicate = { ...first }
|
|
28
|
+
const unrelated = baseMessage({
|
|
29
|
+
text: 'reply',
|
|
30
|
+
role: 'assistant',
|
|
31
|
+
source: {
|
|
32
|
+
platform: 'whatsapp',
|
|
33
|
+
connectorId: 'conn-1',
|
|
34
|
+
connectorName: 'WhatsApp',
|
|
35
|
+
messageId: 'wamid-2',
|
|
36
|
+
senderName: 'Alice',
|
|
37
|
+
},
|
|
38
|
+
historyExcluded: true,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const result = dedupeMessagesForDisplay([first, duplicate, unrelated])
|
|
42
|
+
|
|
43
|
+
assert.equal(result.length, 2)
|
|
44
|
+
assert.equal(result[0].source?.messageId, 'wamid-1')
|
|
45
|
+
assert.equal(result[1].source?.messageId, 'wamid-2')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('dedupeMessagesForDisplay keeps identical connector text from distinct message ids', () => {
|
|
49
|
+
const first = baseMessage({
|
|
50
|
+
source: {
|
|
51
|
+
platform: 'whatsapp',
|
|
52
|
+
connectorId: 'conn-1',
|
|
53
|
+
connectorName: 'WhatsApp',
|
|
54
|
+
messageId: 'wamid-1',
|
|
55
|
+
senderName: 'Alice',
|
|
56
|
+
},
|
|
57
|
+
historyExcluded: true,
|
|
58
|
+
})
|
|
59
|
+
const second = baseMessage({
|
|
60
|
+
source: {
|
|
61
|
+
platform: 'whatsapp',
|
|
62
|
+
connectorId: 'conn-1',
|
|
63
|
+
connectorName: 'WhatsApp',
|
|
64
|
+
messageId: 'wamid-2',
|
|
65
|
+
senderName: 'Alice',
|
|
66
|
+
},
|
|
67
|
+
historyExcluded: true,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const result = dedupeMessagesForDisplay([first, second])
|
|
71
|
+
|
|
72
|
+
assert.equal(result.length, 2)
|
|
73
|
+
assert.deepEqual(result.map((message) => message.source?.messageId), ['wamid-1', 'wamid-2'])
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('formatMessageTimestamp uses exact time formatting for connector transcript entries', () => {
|
|
77
|
+
const now = new Date()
|
|
78
|
+
now.setHours(14, 5, 0, 0)
|
|
79
|
+
const timestamp = now.getTime()
|
|
80
|
+
const formatted = formatMessageTimestamp({
|
|
81
|
+
time: timestamp,
|
|
82
|
+
source: {
|
|
83
|
+
platform: 'whatsapp',
|
|
84
|
+
connectorId: 'conn-1',
|
|
85
|
+
connectorName: 'WhatsApp',
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
assert.match(formatted, /\d{1,2}:\d{2}/)
|
|
90
|
+
assert.doesNotMatch(formatted, /ago|just now/)
|
|
91
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Message } from '@/types'
|
|
2
|
+
|
|
3
|
+
function formatClock(ts: number): string {
|
|
4
|
+
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function formatConnectorTimestamp(ts: number): string {
|
|
8
|
+
const d = new Date(ts)
|
|
9
|
+
const today = new Date()
|
|
10
|
+
if (d.toDateString() === today.toDateString()) return formatClock(ts)
|
|
11
|
+
return d.toLocaleDateString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatRelativeTimestamp(ts: number): string {
|
|
15
|
+
const now = Date.now()
|
|
16
|
+
const diff = now - ts
|
|
17
|
+
if (diff < 60_000) return 'just now'
|
|
18
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
|
|
19
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
|
|
20
|
+
const d = new Date(ts)
|
|
21
|
+
const today = new Date()
|
|
22
|
+
if (d.toDateString() === today.toDateString()) return formatClock(ts)
|
|
23
|
+
if (diff < 604_800_000) return d.toLocaleDateString([], { weekday: 'short', hour: '2-digit', minute: '2-digit' })
|
|
24
|
+
return d.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatMessageTimestamp(message: Pick<Message, 'time' | 'source'>): string {
|
|
28
|
+
if (!message.time) return ''
|
|
29
|
+
if (message.source?.connectorId) return formatConnectorTimestamp(message.time)
|
|
30
|
+
return formatRelativeTimestamp(message.time)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildDisplayDedupKey(message: Message): string | null {
|
|
34
|
+
const source = message.source
|
|
35
|
+
if (source?.connectorId && source.messageId) {
|
|
36
|
+
return [
|
|
37
|
+
message.role,
|
|
38
|
+
source.connectorId,
|
|
39
|
+
source.messageId,
|
|
40
|
+
message.historyExcluded === true ? 'history-excluded' : 'normal',
|
|
41
|
+
].join('|')
|
|
42
|
+
}
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function dedupeMessagesForDisplay(messages: Message[]): Message[] {
|
|
47
|
+
const seen = new Set<string>()
|
|
48
|
+
const deduped: Message[] = []
|
|
49
|
+
for (const message of messages) {
|
|
50
|
+
const key = buildDisplayDedupKey(message)
|
|
51
|
+
if (key) {
|
|
52
|
+
if (seen.has(key)) continue
|
|
53
|
+
seen.add(key)
|
|
54
|
+
}
|
|
55
|
+
deduped.push(message)
|
|
56
|
+
}
|
|
57
|
+
return deduped
|
|
58
|
+
}
|
|
@@ -2,6 +2,7 @@ import { describe, it } from 'node:test'
|
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
3
|
import type { Message } from '@/types'
|
|
4
4
|
import {
|
|
5
|
+
materializeStreamingAssistantArtifacts,
|
|
5
6
|
mergeCompletedAssistantMessage,
|
|
6
7
|
messagesDiffer,
|
|
7
8
|
pruneStreamingAssistantArtifacts,
|
|
@@ -24,7 +25,7 @@ describe('chat-streaming-state', () => {
|
|
|
24
25
|
)
|
|
25
26
|
assert.equal(
|
|
26
27
|
shouldHidePersistedStreamingAssistantMessage(message, { localStreaming: true, displayText: '' }),
|
|
27
|
-
|
|
28
|
+
false,
|
|
28
29
|
)
|
|
29
30
|
})
|
|
30
31
|
|
|
@@ -81,6 +82,51 @@ describe('chat-streaming-state', () => {
|
|
|
81
82
|
])
|
|
82
83
|
})
|
|
83
84
|
|
|
85
|
+
it('materializes stale streaming artifacts into ordinary assistant messages', () => {
|
|
86
|
+
const messages: Message[] = [
|
|
87
|
+
{ role: 'user', text: 'hello', time: 1 },
|
|
88
|
+
{
|
|
89
|
+
role: 'assistant',
|
|
90
|
+
text: 'partial result',
|
|
91
|
+
time: 2,
|
|
92
|
+
streaming: true,
|
|
93
|
+
toolEvents: [{ name: 'browser', input: '{"action":"screenshot"}', output: '/api/uploads/wiki.png' }],
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
const changed = materializeStreamingAssistantArtifacts(messages)
|
|
98
|
+
|
|
99
|
+
assert.equal(changed, true)
|
|
100
|
+
assert.deepEqual(messages, [
|
|
101
|
+
{ role: 'user', text: 'hello', time: 1 },
|
|
102
|
+
{
|
|
103
|
+
role: 'assistant',
|
|
104
|
+
text: 'partial result',
|
|
105
|
+
time: 2,
|
|
106
|
+
streaming: false,
|
|
107
|
+
toolEvents: [{ name: 'browser', input: '{"action":"screenshot"}', output: '/api/uploads/wiki.png' }],
|
|
108
|
+
},
|
|
109
|
+
])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('summarizes tool-only stale streaming artifacts instead of dropping them', () => {
|
|
113
|
+
const messages: Message[] = [
|
|
114
|
+
{ role: 'user', text: 'hello', time: 1 },
|
|
115
|
+
{
|
|
116
|
+
role: 'assistant',
|
|
117
|
+
text: '',
|
|
118
|
+
time: 2,
|
|
119
|
+
streaming: true,
|
|
120
|
+
toolEvents: [{ name: 'browser', input: '{"action":"screenshot"}' }],
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
materializeStreamingAssistantArtifacts(messages)
|
|
125
|
+
|
|
126
|
+
assert.match(messages[1].text, /Started 1 tool call/)
|
|
127
|
+
assert.equal(messages[1].streaming, false)
|
|
128
|
+
})
|
|
129
|
+
|
|
84
130
|
it('reuses the previous assistant slot when the server already persisted the same final text', () => {
|
|
85
131
|
const messages: Message[] = [
|
|
86
132
|
{ role: 'user', text: 'hello', time: 1 },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Message } from '@/types'
|
|
2
|
+
import { buildToolEventAssistantSummary } from '@/lib/tool-event-summary'
|
|
2
3
|
|
|
3
4
|
interface StreamingArtifactWindow {
|
|
4
5
|
minIndex?: number
|
|
@@ -26,6 +27,7 @@ export function shouldHidePersistedStreamingAssistantMessage(
|
|
|
26
27
|
opts.localStreaming
|
|
27
28
|
&& message.role === 'assistant'
|
|
28
29
|
&& message.streaming === true
|
|
30
|
+
&& opts.displayText.trim().length > 0
|
|
29
31
|
)
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -52,6 +54,46 @@ export function upsertStreamingAssistantArtifact(
|
|
|
52
54
|
return true
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
export function materializeStreamingAssistantArtifacts(
|
|
58
|
+
messages: Message[],
|
|
59
|
+
opts: StreamingArtifactWindow = {},
|
|
60
|
+
): boolean {
|
|
61
|
+
let changed = false
|
|
62
|
+
const nextMessages: Message[] = []
|
|
63
|
+
|
|
64
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
65
|
+
const message = messages[index]
|
|
66
|
+
if (!isStreamingAssistantMessage(message, index, opts)) {
|
|
67
|
+
nextMessages.push(message)
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const trimmedText = typeof message.text === 'string' ? message.text.trim() : ''
|
|
72
|
+
const toolEvents = Array.isArray(message.toolEvents) ? message.toolEvents : []
|
|
73
|
+
const thinking = typeof message.thinking === 'string' ? message.thinking.trim() : ''
|
|
74
|
+
const fallbackText = !trimmedText && toolEvents.length > 0
|
|
75
|
+
? buildToolEventAssistantSummary(toolEvents, { interrupted: true })
|
|
76
|
+
: ''
|
|
77
|
+
const nextText = trimmedText || fallbackText
|
|
78
|
+
|
|
79
|
+
if (!nextText && !thinking && toolEvents.length === 0) {
|
|
80
|
+
changed = true
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
nextMessages.push({
|
|
85
|
+
...message,
|
|
86
|
+
text: nextText,
|
|
87
|
+
streaming: false,
|
|
88
|
+
})
|
|
89
|
+
changed = true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!changed) return false
|
|
93
|
+
messages.splice(0, messages.length, ...nextMessages)
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
|
|
55
97
|
export function mergeCompletedAssistantMessage(messages: Message[], assistantMessage: Message): Message[] {
|
|
56
98
|
let end = messages.length
|
|
57
99
|
while (end > 0) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const OLLAMA_CLOUD_MODEL_SUFFIX = ':cloud'
|
|
2
|
+
|
|
3
|
+
export function isOllamaCloudModel(model: string | null | undefined): boolean {
|
|
4
|
+
return typeof model === 'string' && /:cloud$/i.test(model.trim())
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function stripOllamaCloudModelSuffix(model: string | null | undefined): string {
|
|
8
|
+
if (typeof model !== 'string') return ''
|
|
9
|
+
return model.trim().replace(/:cloud$/i, '')
|
|
10
|
+
}
|
|
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
|
|
|
2
2
|
import { test } from 'node:test'
|
|
3
3
|
import {
|
|
4
4
|
deriveOpenClawWsUrl,
|
|
5
|
+
isLocalOpenClawEndpoint,
|
|
5
6
|
normalizeOpenClawEndpoint,
|
|
6
7
|
normalizeProviderEndpoint,
|
|
7
8
|
} from './openclaw-endpoint.ts'
|
|
@@ -46,3 +47,10 @@ test('normalizeProviderEndpoint only rewrites openclaw provider', () => {
|
|
|
46
47
|
null,
|
|
47
48
|
)
|
|
48
49
|
})
|
|
50
|
+
|
|
51
|
+
test('isLocalOpenClawEndpoint detects loopback hosts', () => {
|
|
52
|
+
assert.equal(isLocalOpenClawEndpoint('ws://localhost:18789'), true)
|
|
53
|
+
assert.equal(isLocalOpenClawEndpoint('http://127.0.0.1:18789/v1'), true)
|
|
54
|
+
assert.equal(isLocalOpenClawEndpoint('http://[::1]:18789/v1'), true)
|
|
55
|
+
assert.equal(isLocalOpenClawEndpoint('https://openclaw.example.com/v1'), false)
|
|
56
|
+
})
|
|
@@ -57,6 +57,12 @@ export function deriveOpenClawWsUrl(input?: string | null): string {
|
|
|
57
57
|
return value.endsWith('/') ? value.slice(0, -1) : value
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
export function isLocalOpenClawEndpoint(input?: string | null): boolean {
|
|
61
|
+
const parsed = parseUrl(input || '') || parseUrl(DEFAULT_OPENCLAW_ENDPOINT)!
|
|
62
|
+
const host = parsed.hostname.trim().toLowerCase().replace(/^\[(.*)\]$/, '$1')
|
|
63
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '0.0.0.0'
|
|
64
|
+
}
|
|
65
|
+
|
|
60
66
|
export function normalizeProviderEndpoint(provider: string | null | undefined, endpoint: string | null | undefined): string | null {
|
|
61
67
|
if (typeof endpoint !== 'string') return null
|
|
62
68
|
const trimmed = endpoint.trim()
|
|
@@ -64,4 +70,3 @@ export function normalizeProviderEndpoint(provider: string | null | undefined, e
|
|
|
64
70
|
if (provider === 'openclaw') return normalizeOpenClawEndpoint(trimmed)
|
|
65
71
|
return trimmed.replace(/\/+$/, '')
|
|
66
72
|
}
|
|
67
|
-
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const DEFAULT_ALLOWED_ORIGINS = [
|
|
2
|
+
'https://swarmclaw.ai',
|
|
3
|
+
'https://www.swarmclaw.ai',
|
|
4
|
+
'http://localhost:3000',
|
|
5
|
+
'http://127.0.0.1:3000',
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
function parseAllowedOrigins(raw: string | undefined): string[] {
|
|
9
|
+
if (!raw) return DEFAULT_ALLOWED_ORIGINS
|
|
10
|
+
const parsed = raw
|
|
11
|
+
.split(',')
|
|
12
|
+
.map((entry) => entry.trim())
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
return parsed.length > 0 ? parsed : DEFAULT_ALLOWED_ORIGINS
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeOrigin(raw: string | null | undefined): string {
|
|
18
|
+
if (!raw) return ''
|
|
19
|
+
try {
|
|
20
|
+
return new URL(raw).origin
|
|
21
|
+
} catch {
|
|
22
|
+
return ''
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolvePluginInstallCorsOrigin(rawOrigin: string | null | undefined): string | null {
|
|
27
|
+
const origin = normalizeOrigin(rawOrigin)
|
|
28
|
+
if (!origin) return null
|
|
29
|
+
const allowed = parseAllowedOrigins(process.env.SWARMCLAW_PLUGIN_INSTALL_ORIGINS)
|
|
30
|
+
return allowed.includes(origin) ? origin : null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildPluginInstallCorsHeaders(origin: string | null): HeadersInit {
|
|
34
|
+
const headers = new Headers()
|
|
35
|
+
headers.set('Vary', 'Origin')
|
|
36
|
+
if (!origin) return headers
|
|
37
|
+
headers.set('Access-Control-Allow-Origin', origin)
|
|
38
|
+
headers.set('Access-Control-Allow-Headers', 'Content-Type, X-Access-Key')
|
|
39
|
+
headers.set('Access-Control-Allow-Methods', 'POST, OPTIONS')
|
|
40
|
+
headers.set('Access-Control-Max-Age', '600')
|
|
41
|
+
return headers
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isPluginInstallCorsPath(pathname: string): boolean {
|
|
45
|
+
return pathname === '/api/plugins/install'
|
|
46
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getPluginSourceLabel,
|
|
6
|
+
inferPluginInstallSourceFromUrl,
|
|
7
|
+
inferPluginPublisherSourceFromUrl,
|
|
8
|
+
isMarketplaceInstallSource,
|
|
9
|
+
normalizePluginCatalogSource,
|
|
10
|
+
normalizePluginInstallSource,
|
|
11
|
+
normalizePluginPublisherSource,
|
|
12
|
+
} from './plugin-sources'
|
|
13
|
+
|
|
14
|
+
describe('plugin source helpers', () => {
|
|
15
|
+
it('normalizes publisher, catalog, and install source values', () => {
|
|
16
|
+
assert.equal(normalizePluginPublisherSource('SwarmForge'), 'swarmforge')
|
|
17
|
+
assert.equal(normalizePluginCatalogSource('swarmclaw-site'), 'swarmclaw-site')
|
|
18
|
+
assert.equal(normalizePluginInstallSource('ClawHub'), 'clawhub')
|
|
19
|
+
assert.equal(normalizePluginInstallSource('unknown-source'), undefined)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('infers plugin provenance from known marketplace URLs', () => {
|
|
23
|
+
assert.equal(
|
|
24
|
+
inferPluginPublisherSourceFromUrl('https://raw.githubusercontent.com/swarmclawai/swarmforge/main/tool-logger.js'),
|
|
25
|
+
'swarmforge',
|
|
26
|
+
)
|
|
27
|
+
assert.equal(
|
|
28
|
+
inferPluginInstallSourceFromUrl('https://clawhub.ai/skills/openclaw-gmail'),
|
|
29
|
+
'clawhub',
|
|
30
|
+
)
|
|
31
|
+
assert.equal(
|
|
32
|
+
inferPluginPublisherSourceFromUrl('https://swarmclaw.ai/plugins/demo.js'),
|
|
33
|
+
'swarmclaw',
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('labels marketplace sources consistently', () => {
|
|
38
|
+
assert.equal(isMarketplaceInstallSource('swarmclaw-site'), true)
|
|
39
|
+
assert.equal(isMarketplaceInstallSource('manual'), false)
|
|
40
|
+
assert.equal(getPluginSourceLabel('swarmclaw-site'), 'SwarmClaw Site')
|
|
41
|
+
assert.equal(getPluginSourceLabel('swarmforge'), 'SwarmForge')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PluginCatalogSource,
|
|
3
|
+
PluginInstallSource,
|
|
4
|
+
PluginPublisherSource,
|
|
5
|
+
} from '@/types'
|
|
6
|
+
|
|
7
|
+
const PUBLISHER_SOURCES = ['builtin', 'local', 'manual', 'swarmclaw', 'swarmforge', 'clawhub'] as const
|
|
8
|
+
const CATALOG_SOURCES = ['swarmclaw', 'swarmclaw-site', 'swarmforge', 'clawhub'] as const
|
|
9
|
+
const INSTALL_SOURCES = ['builtin', 'local', 'manual', ...CATALOG_SOURCES] as const
|
|
10
|
+
|
|
11
|
+
const SOURCE_LABELS: Record<PluginInstallSource | PluginPublisherSource, string> = {
|
|
12
|
+
builtin: 'Built-in',
|
|
13
|
+
local: 'Local file',
|
|
14
|
+
manual: 'Manual URL',
|
|
15
|
+
swarmclaw: 'SwarmClaw',
|
|
16
|
+
'swarmclaw-site': 'SwarmClaw Site',
|
|
17
|
+
swarmforge: 'SwarmForge',
|
|
18
|
+
clawhub: 'ClawHub',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizePluginPublisherSource(raw: unknown): PluginPublisherSource | undefined {
|
|
22
|
+
const value = typeof raw === 'string' ? raw.trim().toLowerCase() : ''
|
|
23
|
+
if (!value) return undefined
|
|
24
|
+
return (PUBLISHER_SOURCES as readonly string[]).includes(value)
|
|
25
|
+
? value as PluginPublisherSource
|
|
26
|
+
: undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function normalizePluginCatalogSource(raw: unknown): PluginCatalogSource | undefined {
|
|
30
|
+
const value = typeof raw === 'string' ? raw.trim().toLowerCase() : ''
|
|
31
|
+
if (!value) return undefined
|
|
32
|
+
return (CATALOG_SOURCES as readonly string[]).includes(value)
|
|
33
|
+
? value as PluginCatalogSource
|
|
34
|
+
: undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function normalizePluginInstallSource(raw: unknown): PluginInstallSource | undefined {
|
|
38
|
+
const value = typeof raw === 'string' ? raw.trim().toLowerCase() : ''
|
|
39
|
+
if (!value) return undefined
|
|
40
|
+
return (INSTALL_SOURCES as readonly string[]).includes(value)
|
|
41
|
+
? value as PluginInstallSource
|
|
42
|
+
: undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function inferPluginPublisherSourceFromUrl(url: string | null | undefined): PluginPublisherSource | undefined {
|
|
46
|
+
const normalized = typeof url === 'string' ? url.trim().toLowerCase() : ''
|
|
47
|
+
if (!normalized) return undefined
|
|
48
|
+
if (normalized.includes('clawhub.ai')) return 'clawhub'
|
|
49
|
+
if (normalized.includes('swarmclaw.ai/')) return 'swarmclaw'
|
|
50
|
+
if (
|
|
51
|
+
normalized.includes('raw.githubusercontent.com/swarmclawai/swarmforge/')
|
|
52
|
+
|| normalized.includes('github.com/swarmclawai/swarmforge/')
|
|
53
|
+
|| normalized.includes('/swarmclawai/plugins/')
|
|
54
|
+
) {
|
|
55
|
+
return 'swarmforge'
|
|
56
|
+
}
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function inferPluginInstallSourceFromUrl(url: string | null | undefined): PluginInstallSource | undefined {
|
|
61
|
+
const publisherSource = inferPluginPublisherSourceFromUrl(url)
|
|
62
|
+
if (publisherSource === 'swarmclaw' || publisherSource === 'swarmforge' || publisherSource === 'clawhub') {
|
|
63
|
+
return publisherSource
|
|
64
|
+
}
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function isMarketplaceInstallSource(source: PluginInstallSource | null | undefined): boolean {
|
|
69
|
+
return source === 'swarmclaw' || source === 'swarmclaw-site' || source === 'swarmforge' || source === 'clawhub'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getPluginSourceLabel(
|
|
73
|
+
source: PluginInstallSource | PluginPublisherSource | PluginCatalogSource | null | undefined,
|
|
74
|
+
): string {
|
|
75
|
+
if (!source) return 'Unknown'
|
|
76
|
+
return SOURCE_LABELS[source as keyof typeof SOURCE_LABELS] || source
|
|
77
|
+
}
|