@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,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace context injection — injects workspace files into the agent's system prompt.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by OpenClaw's pattern of injecting HEARTBEAT.md, IDENTITY.md, AGENTS.md,
|
|
5
|
+
* SOUL.md, TOOLS.md, USER.md, and BOOTSTRAP.md into every agent turn.
|
|
6
|
+
*
|
|
7
|
+
* This gives agents self-awareness, goals, and context about their operating environment
|
|
8
|
+
* without requiring the user to manually configure everything.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs'
|
|
12
|
+
import path from 'path'
|
|
13
|
+
import { WORKSPACE_DIR } from './data-dir'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Workspace files to inject, in priority order.
|
|
17
|
+
* Higher-priority files are injected first and get more budget.
|
|
18
|
+
*/
|
|
19
|
+
const WORKSPACE_FILES = [
|
|
20
|
+
{ name: 'HEARTBEAT.md', maxChars: 2000, section: 'Active Tasks & Heartbeat' },
|
|
21
|
+
{ name: 'IDENTITY.md', maxChars: 800, section: 'Agent Identity' },
|
|
22
|
+
{ name: 'AGENTS.md', maxChars: 2000, section: 'Agent Directory' },
|
|
23
|
+
{ name: 'BOOTSTRAP.md', maxChars: 1500, section: 'Bootstrap Instructions' },
|
|
24
|
+
{ name: 'TOOLS.md', maxChars: 1000, section: 'Tool Configuration' },
|
|
25
|
+
{ name: 'USER.md', maxChars: 500, section: 'User Preferences' },
|
|
26
|
+
] as const
|
|
27
|
+
|
|
28
|
+
const TOTAL_MAX_CHARS = 8000
|
|
29
|
+
|
|
30
|
+
interface WorkspaceContextOptions {
|
|
31
|
+
/** Session working directory (overrides global workspace) */
|
|
32
|
+
cwd?: string | null
|
|
33
|
+
/** Maximum total characters for all workspace files */
|
|
34
|
+
maxTotalChars?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface InjectedFile {
|
|
38
|
+
name: string
|
|
39
|
+
chars: number
|
|
40
|
+
truncated: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface WorkspaceContextResult {
|
|
44
|
+
/** The assembled context block to inject into the system prompt */
|
|
45
|
+
block: string
|
|
46
|
+
/** Which files were injected and their sizes */
|
|
47
|
+
files: InjectedFile[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readFileSafe(filePath: string): string | null {
|
|
51
|
+
try {
|
|
52
|
+
if (!fs.existsSync(filePath)) return null
|
|
53
|
+
const stat = fs.statSync(filePath)
|
|
54
|
+
// Skip files over 50KB
|
|
55
|
+
if (stat.size > 50_000) return null
|
|
56
|
+
return fs.readFileSync(filePath, 'utf-8').trim()
|
|
57
|
+
} catch {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if content is effectively empty (only headers, empty list items, whitespace).
|
|
64
|
+
*/
|
|
65
|
+
function isEffectivelyEmpty(content: string): boolean {
|
|
66
|
+
for (const line of content.split('\n')) {
|
|
67
|
+
const trimmed = line.trim()
|
|
68
|
+
if (!trimmed) continue
|
|
69
|
+
if (/^#+(\s|$)/.test(trimmed)) continue
|
|
70
|
+
if (/^[-*+]\s*(\[[\sXx]?\]\s*)?$/.test(trimmed)) continue
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build workspace context for injection into the agent's system prompt.
|
|
78
|
+
* Reads workspace files and assembles them into a single context block.
|
|
79
|
+
*/
|
|
80
|
+
export function buildWorkspaceContext(opts: WorkspaceContextOptions = {}): WorkspaceContextResult {
|
|
81
|
+
const workspaceDir = opts.cwd || WORKSPACE_DIR
|
|
82
|
+
const maxTotal = opts.maxTotalChars || TOTAL_MAX_CHARS
|
|
83
|
+
const files: InjectedFile[] = []
|
|
84
|
+
const sections: string[] = []
|
|
85
|
+
let totalChars = 0
|
|
86
|
+
|
|
87
|
+
for (const spec of WORKSPACE_FILES) {
|
|
88
|
+
if (totalChars >= maxTotal) break
|
|
89
|
+
|
|
90
|
+
const filePath = path.join(workspaceDir, spec.name)
|
|
91
|
+
const content = readFileSafe(filePath)
|
|
92
|
+
if (!content || isEffectivelyEmpty(content)) continue
|
|
93
|
+
|
|
94
|
+
const budget = Math.min(spec.maxChars, maxTotal - totalChars)
|
|
95
|
+
if (budget <= 0) break
|
|
96
|
+
|
|
97
|
+
const truncated = content.length > budget
|
|
98
|
+
const injected = truncated ? content.slice(0, budget) + '\n[...truncated]' : content
|
|
99
|
+
|
|
100
|
+
sections.push(`## ${spec.section}\n_Source: ${spec.name}_\n${injected}`)
|
|
101
|
+
files.push({ name: spec.name, chars: injected.length, truncated })
|
|
102
|
+
totalChars += injected.length
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (sections.length === 0) {
|
|
106
|
+
return { block: '', files: [] }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const block = `# Workspace Context\n${sections.join('\n\n')}`
|
|
110
|
+
return { block, files }
|
|
111
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { buildSkillSavePayload } from './skill-save-payload'
|
|
5
|
+
|
|
6
|
+
test('buildSkillSavePayload preserves imported skill metadata when saving', () => {
|
|
7
|
+
const payload = buildSkillSavePayload({
|
|
8
|
+
name: 'GitHub Sync',
|
|
9
|
+
filename: 'github-sync.md',
|
|
10
|
+
description: 'Sync GitHub issues into tasks.',
|
|
11
|
+
content: '# Sync issues',
|
|
12
|
+
scope: 'agent',
|
|
13
|
+
agentIds: ['agent-1'],
|
|
14
|
+
}, {
|
|
15
|
+
sourceUrl: 'https://example.com/SKILL.md',
|
|
16
|
+
sourceFormat: 'openclaw',
|
|
17
|
+
author: 'Codex',
|
|
18
|
+
tags: ['github', 'tasks'],
|
|
19
|
+
version: '1.2.3',
|
|
20
|
+
homepage: 'https://example.com/github-sync',
|
|
21
|
+
primaryEnv: 'GITHUB_TOKEN',
|
|
22
|
+
skillKey: 'github-sync',
|
|
23
|
+
always: true,
|
|
24
|
+
installOptions: [{ kind: 'brew', label: 'gh', bins: ['gh'] }],
|
|
25
|
+
skillRequirements: { env: ['GITHUB_TOKEN'] },
|
|
26
|
+
detectedEnvVars: ['GITHUB_TOKEN'],
|
|
27
|
+
security: { level: 'medium', notes: ['Review install steps.'] },
|
|
28
|
+
frontmatter: { name: 'github-sync', metadata: { openclaw: { primaryEnv: 'GITHUB_TOKEN' } } },
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
assert.equal(payload.sourceFormat, 'openclaw')
|
|
32
|
+
assert.equal(payload.version, '1.2.3')
|
|
33
|
+
assert.equal(payload.primaryEnv, 'GITHUB_TOKEN')
|
|
34
|
+
assert.equal(payload.skillKey, 'github-sync')
|
|
35
|
+
assert.deepEqual(payload.agentIds, ['agent-1'])
|
|
36
|
+
assert.deepEqual(payload.skillRequirements, { env: ['GITHUB_TOKEN'] })
|
|
37
|
+
assert.deepEqual(payload.security, { level: 'medium', notes: ['Review install steps.'] })
|
|
38
|
+
assert.deepEqual(payload.frontmatter, { name: 'github-sync', metadata: { openclaw: { primaryEnv: 'GITHUB_TOKEN' } } })
|
|
39
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Skill } from '@/types'
|
|
2
|
+
|
|
3
|
+
export type SkillScope = 'global' | 'agent'
|
|
4
|
+
|
|
5
|
+
export interface SkillDraftInput {
|
|
6
|
+
name: string
|
|
7
|
+
filename: string
|
|
8
|
+
description: string
|
|
9
|
+
content: string
|
|
10
|
+
scope: SkillScope
|
|
11
|
+
agentIds: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildSkillSavePayload(draft: SkillDraftInput, metadataPreview?: Partial<Skill> | null) {
|
|
15
|
+
return {
|
|
16
|
+
name: draft.name.trim() || 'Unnamed Skill',
|
|
17
|
+
filename: draft.filename.trim() || `${draft.name.trim().toLowerCase().replace(/\s+/g, '-')}.md`,
|
|
18
|
+
description: draft.description,
|
|
19
|
+
content: draft.content,
|
|
20
|
+
scope: draft.scope,
|
|
21
|
+
agentIds: draft.scope === 'agent' ? draft.agentIds : [],
|
|
22
|
+
sourceUrl: metadataPreview?.sourceUrl,
|
|
23
|
+
sourceFormat: metadataPreview?.sourceFormat,
|
|
24
|
+
author: metadataPreview?.author,
|
|
25
|
+
tags: metadataPreview?.tags,
|
|
26
|
+
version: metadataPreview?.version,
|
|
27
|
+
homepage: metadataPreview?.homepage,
|
|
28
|
+
primaryEnv: metadataPreview?.primaryEnv,
|
|
29
|
+
skillKey: metadataPreview?.skillKey,
|
|
30
|
+
always: metadataPreview?.always,
|
|
31
|
+
installOptions: metadataPreview?.installOptions,
|
|
32
|
+
skillRequirements: metadataPreview?.skillRequirements,
|
|
33
|
+
detectedEnvVars: metadataPreview?.detectedEnvVars,
|
|
34
|
+
security: metadataPreview?.security,
|
|
35
|
+
frontmatter: metadataPreview?.frontmatter,
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/lib/tasks.ts
CHANGED
|
@@ -4,6 +4,31 @@ import type { BoardTask } from '../types'
|
|
|
4
4
|
export const fetchTasks = (includeArchived = false) =>
|
|
5
5
|
api<Record<string, BoardTask>>('GET', `/tasks${includeArchived ? '?includeArchived=true' : ''}`)
|
|
6
6
|
|
|
7
|
+
export interface GitHubIssueImportRequest {
|
|
8
|
+
repo: string
|
|
9
|
+
token?: string
|
|
10
|
+
state?: 'open' | 'closed' | 'all'
|
|
11
|
+
limit?: number
|
|
12
|
+
labels?: string[]
|
|
13
|
+
projectId?: string | null
|
|
14
|
+
agentId?: string | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface GitHubIssueImportItem {
|
|
18
|
+
taskId?: string
|
|
19
|
+
number: number
|
|
20
|
+
title: string
|
|
21
|
+
url: string | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GitHubIssueImportResult {
|
|
25
|
+
repo: string
|
|
26
|
+
state: 'open' | 'closed' | 'all'
|
|
27
|
+
fetched: number
|
|
28
|
+
created: GitHubIssueImportItem[]
|
|
29
|
+
skipped: GitHubIssueImportItem[]
|
|
30
|
+
}
|
|
31
|
+
|
|
7
32
|
export const createTask = (data: {
|
|
8
33
|
title: string
|
|
9
34
|
description: string
|
|
@@ -27,3 +52,6 @@ export const unarchiveTask = (id: string) =>
|
|
|
27
52
|
|
|
28
53
|
export const bulkUpdateTasks = (ids: string[], data: { status?: string; agentId?: string | null; projectId?: string | null }) =>
|
|
29
54
|
api<{ updated: number; ids: string[] }>('POST', '/tasks/bulk', { ids, ...data })
|
|
55
|
+
|
|
56
|
+
export const importGitHubIssues = (data: GitHubIssueImportRequest) =>
|
|
57
|
+
api<GitHubIssueImportResult>('POST', '/tasks/import/github', data, { timeoutMs: 30_000 })
|
|
@@ -36,7 +36,8 @@ export const AVAILABLE_TOOLS: ToolDefinition[] = [
|
|
|
36
36
|
* Granular CRUD tools are now unified under 'manage_platform'.
|
|
37
37
|
*/
|
|
38
38
|
export const PLATFORM_TOOLS: ToolDefinition[] = [
|
|
39
|
-
{ id: 'manage_platform', label: 'Platform', description: 'Unified management of agents, tasks, schedules, skills, documents, and secrets' },
|
|
39
|
+
{ id: 'manage_platform', label: 'Platform', description: 'Unified management of agents, projects, tasks, schedules, skills, documents, and secrets' },
|
|
40
|
+
{ id: 'manage_projects', label: 'Projects', description: 'Manage durable project context: objectives, priorities, heartbeat plans, credential needs, and linked resources' },
|
|
40
41
|
{ id: 'manage_connectors', label: 'Connectors', description: 'Manage chat platform bridges and send outbound messages' },
|
|
41
42
|
{ id: 'manage_chatrooms', label: 'Chatrooms', description: 'Manage SwarmClaw routing rules and multi-agent chatrooms' },
|
|
42
43
|
{ id: 'delegate_to_agent', label: 'Assign Agent', description: 'Delegate a task to another specific agent' },
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { buildToolEventAssistantSummary } from './tool-event-summary'
|
|
5
|
+
|
|
6
|
+
describe('buildToolEventAssistantSummary', () => {
|
|
7
|
+
it('summarizes completed tool-only runs', () => {
|
|
8
|
+
const summary = buildToolEventAssistantSummary([
|
|
9
|
+
{ name: 'browser', input: '{"action":"screenshot"}', output: '/api/uploads/wiki.png' },
|
|
10
|
+
{ name: 'send_file', input: '{"filePath":"wiki.png"}', output: '[wiki](/api/uploads/wiki.png)' },
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
assert.equal(
|
|
14
|
+
summary,
|
|
15
|
+
'Used 2 tool calls (`browser`, `send_file`). See tool output above for details.',
|
|
16
|
+
)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('summarizes interrupted in-flight tool runs', () => {
|
|
20
|
+
const summary = buildToolEventAssistantSummary(
|
|
21
|
+
[{ name: 'browser', input: '{"action":"navigate"}' }],
|
|
22
|
+
{ interrupted: true },
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
assert.equal(
|
|
26
|
+
summary,
|
|
27
|
+
'Started 1 tool call (`browser`). Progress was interrupted before completion.',
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { MessageToolEvent } from '@/types'
|
|
2
|
+
|
|
3
|
+
interface ToolEventAssistantSummaryOptions {
|
|
4
|
+
interrupted?: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function buildToolEventAssistantSummary(
|
|
8
|
+
toolEvents: MessageToolEvent[] | undefined,
|
|
9
|
+
options: ToolEventAssistantSummaryOptions = {},
|
|
10
|
+
): string {
|
|
11
|
+
const events = Array.isArray(toolEvents)
|
|
12
|
+
? toolEvents.filter((event) => typeof event?.name === 'string' && event.name.trim().length > 0)
|
|
13
|
+
: []
|
|
14
|
+
if (events.length === 0) return ''
|
|
15
|
+
|
|
16
|
+
const uniqueNames = [...new Set(events.map((event) => event.name.trim()))]
|
|
17
|
+
const visibleNames = uniqueNames.slice(0, 4).map((name) => `\`${name}\``).join(', ')
|
|
18
|
+
const hiddenCount = Math.max(0, uniqueNames.length - 4)
|
|
19
|
+
const pendingCount = events.filter((event) => !event.output).length
|
|
20
|
+
const errorCount = events.filter((event) => event.error === true).length
|
|
21
|
+
const toolWord = events.length === 1 ? 'tool call' : 'tool calls'
|
|
22
|
+
const interrupted = options.interrupted === true
|
|
23
|
+
|
|
24
|
+
const namesLabel = hiddenCount > 0
|
|
25
|
+
? `${visibleNames}, +${hiddenCount} more`
|
|
26
|
+
: visibleNames
|
|
27
|
+
|
|
28
|
+
if (interrupted || pendingCount > 0) {
|
|
29
|
+
return `Started ${events.length} ${toolWord} (${namesLabel}). Progress was interrupted before completion.`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (errorCount > 0) {
|
|
33
|
+
return `Used ${events.length} ${toolWord} (${namesLabel}). ${errorCount} ${errorCount === 1 ? 'call reported an error' : 'calls reported errors'}. See tool output above for details.`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return `Used ${events.length} ${toolWord} (${namesLabel}). See tool output above for details.`
|
|
37
|
+
}
|
|
@@ -43,6 +43,7 @@ export const AgentCreateSchema = z.object({
|
|
|
43
43
|
thinkingLevel: z.string().optional(),
|
|
44
44
|
soul: z.string().optional(),
|
|
45
45
|
identityState: z.record(z.string(), z.unknown()).nullable().optional().default(null),
|
|
46
|
+
disabled: z.boolean().optional().default(false),
|
|
46
47
|
heartbeatEnabled: z.boolean().optional().default(false),
|
|
47
48
|
heartbeatInterval: z.union([z.string(), z.number()]).nullable().optional().default(null),
|
|
48
49
|
heartbeatIntervalSec: z.number().int().nonnegative().nullable().optional().default(null),
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import type { WalletTransaction } from '@/types'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
filterWalletTransactions,
|
|
8
|
+
getWalletTransactionStatusGroup,
|
|
9
|
+
matchesWalletTransactionFilter,
|
|
10
|
+
matchesWalletTransactionQuery,
|
|
11
|
+
} from './wallet-transactions'
|
|
12
|
+
|
|
13
|
+
function buildTransaction(overrides: Partial<WalletTransaction> = {}): WalletTransaction {
|
|
14
|
+
return {
|
|
15
|
+
id: 'tx-1',
|
|
16
|
+
walletId: 'wallet-1',
|
|
17
|
+
agentId: 'agent-1',
|
|
18
|
+
chain: 'ethereum',
|
|
19
|
+
type: 'swap',
|
|
20
|
+
signature: '0xabc123',
|
|
21
|
+
fromAddress: '0xfrom000000000000000000000000000000000001',
|
|
22
|
+
toAddress: '0xto0000000000000000000000000000000000002',
|
|
23
|
+
amountAtomic: '1000000',
|
|
24
|
+
status: 'confirmed',
|
|
25
|
+
memo: 'Swapped 1 USDC to ETH',
|
|
26
|
+
timestamp: 1,
|
|
27
|
+
...overrides,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('wallet transaction filters', () => {
|
|
32
|
+
it('groups pending and pending_approval together', () => {
|
|
33
|
+
assert.equal(getWalletTransactionStatusGroup('pending'), 'pending')
|
|
34
|
+
assert.equal(getWalletTransactionStatusGroup('pending_approval'), 'pending')
|
|
35
|
+
assert.equal(getWalletTransactionStatusGroup('confirmed'), 'confirmed')
|
|
36
|
+
assert.equal(getWalletTransactionStatusGroup('failed'), 'failed')
|
|
37
|
+
assert.equal(getWalletTransactionStatusGroup('denied'), 'failed')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('matches filters by type and status group', () => {
|
|
41
|
+
assert.equal(matchesWalletTransactionFilter(buildTransaction({ type: 'swap' }), 'swap'), true)
|
|
42
|
+
assert.equal(matchesWalletTransactionFilter(buildTransaction({ type: 'send' }), 'send'), true)
|
|
43
|
+
assert.equal(matchesWalletTransactionFilter(buildTransaction({ status: 'pending_approval' }), 'pending'), true)
|
|
44
|
+
assert.equal(matchesWalletTransactionFilter(buildTransaction({ status: 'denied' }), 'failed'), true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('matches search queries against signature, memo, and addresses', () => {
|
|
48
|
+
const tx = buildTransaction()
|
|
49
|
+
assert.equal(matchesWalletTransactionQuery(tx, 'usdc'), true)
|
|
50
|
+
assert.equal(matchesWalletTransactionQuery(tx, '0xabc123'), true)
|
|
51
|
+
assert.equal(matchesWalletTransactionQuery(tx, '0xfrom0000'), true)
|
|
52
|
+
assert.equal(matchesWalletTransactionQuery(tx, 'missing-text'), false)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('filters transactions by combined status/type and search query', () => {
|
|
56
|
+
const transactions = [
|
|
57
|
+
buildTransaction({ id: 'tx-confirmed', status: 'confirmed', memo: 'Swap USDC to ETH' }),
|
|
58
|
+
buildTransaction({ id: 'tx-pending', status: 'pending_approval', memo: 'Awaiting approval' }),
|
|
59
|
+
buildTransaction({ id: 'tx-send', type: 'send', status: 'confirmed', memo: 'Send ETH to treasury' }),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
assert.deepEqual(
|
|
63
|
+
filterWalletTransactions(transactions, { filter: 'pending' }).map((tx) => tx.id),
|
|
64
|
+
['tx-pending'],
|
|
65
|
+
)
|
|
66
|
+
assert.deepEqual(
|
|
67
|
+
filterWalletTransactions(transactions, { filter: 'send', query: 'treasury' }).map((tx) => tx.id),
|
|
68
|
+
['tx-send'],
|
|
69
|
+
)
|
|
70
|
+
assert.deepEqual(
|
|
71
|
+
filterWalletTransactions(transactions, { filter: 'confirmed', query: 'usdc' }).map((tx) => tx.id),
|
|
72
|
+
['tx-confirmed'],
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { WalletTransaction, WalletTransactionStatus } from '@/types'
|
|
2
|
+
|
|
3
|
+
export type WalletTransactionFilter = 'all' | 'confirmed' | 'pending' | 'failed' | 'send' | 'receive' | 'swap'
|
|
4
|
+
|
|
5
|
+
export function getWalletTransactionStatusGroup(status: WalletTransactionStatus): 'confirmed' | 'pending' | 'failed' {
|
|
6
|
+
if (status === 'confirmed') return 'confirmed'
|
|
7
|
+
if (status === 'pending' || status === 'pending_approval') return 'pending'
|
|
8
|
+
return 'failed'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function matchesWalletTransactionFilter(tx: WalletTransaction, filter: WalletTransactionFilter): boolean {
|
|
12
|
+
if (filter === 'all') return true
|
|
13
|
+
if (filter === 'send' || filter === 'receive' || filter === 'swap') return tx.type === filter
|
|
14
|
+
return getWalletTransactionStatusGroup(tx.status) === filter
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function matchesWalletTransactionQuery(tx: WalletTransaction, query: string): boolean {
|
|
18
|
+
const normalized = query.trim().toLowerCase()
|
|
19
|
+
if (!normalized) return true
|
|
20
|
+
const haystack = [
|
|
21
|
+
tx.id,
|
|
22
|
+
tx.signature,
|
|
23
|
+
tx.memo || '',
|
|
24
|
+
tx.fromAddress,
|
|
25
|
+
tx.toAddress,
|
|
26
|
+
tx.status,
|
|
27
|
+
tx.type,
|
|
28
|
+
tx.tokenMint || '',
|
|
29
|
+
tx.approvedBy || '',
|
|
30
|
+
]
|
|
31
|
+
.join(' ')
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
return haystack.includes(normalized)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function filterWalletTransactions(
|
|
37
|
+
transactions: WalletTransaction[],
|
|
38
|
+
options?: { filter?: WalletTransactionFilter; query?: string },
|
|
39
|
+
): WalletTransaction[] {
|
|
40
|
+
const filter = options?.filter || 'all'
|
|
41
|
+
const query = options?.query || ''
|
|
42
|
+
return transactions.filter((tx) => matchesWalletTransactionFilter(tx, filter) && matchesWalletTransactionQuery(tx, query))
|
|
43
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { getWalletChainOrDefault, parseDisplayAmountToAtomic } from './wallet'
|
|
5
|
+
|
|
6
|
+
describe('wallet helpers', () => {
|
|
7
|
+
it('rejects unsupported wallet chain aliases instead of silently remapping them', () => {
|
|
8
|
+
assert.throws(() => getWalletChainOrDefault('base'), /Unsupported wallet chain or provider: base/)
|
|
9
|
+
assert.throws(() => getWalletChainOrDefault('ethereun'), /Unsupported wallet chain or provider: ethereun/)
|
|
10
|
+
assert.equal(getWalletChainOrDefault(undefined), 'solana')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('parses tiny ETH amounts from either strings or numbers', () => {
|
|
14
|
+
assert.equal(parseDisplayAmountToAtomic('0.000000000000000001', 18), '1')
|
|
15
|
+
assert.equal(parseDisplayAmountToAtomic(0.000000000000000001, 18), '1')
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { WalletChain } from '@/types'
|
|
2
|
+
|
|
3
|
+
export const SUPPORTED_WALLET_CHAINS = ['solana', 'ethereum'] as const
|
|
4
|
+
|
|
5
|
+
export interface WalletChainMeta {
|
|
6
|
+
chain: WalletChain
|
|
7
|
+
label: string
|
|
8
|
+
symbol: string
|
|
9
|
+
decimals: number
|
|
10
|
+
defaultPerTxAtomic: string
|
|
11
|
+
defaultDailyAtomic: string
|
|
12
|
+
addressExplorerBaseUrl: string
|
|
13
|
+
transactionExplorerBaseUrl: string
|
|
14
|
+
createDescription: string
|
|
15
|
+
fundingInstructions: string[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const WALLET_CHAIN_META: Record<WalletChain, WalletChainMeta> = {
|
|
19
|
+
solana: {
|
|
20
|
+
chain: 'solana',
|
|
21
|
+
label: 'Solana',
|
|
22
|
+
symbol: 'SOL',
|
|
23
|
+
decimals: 9,
|
|
24
|
+
defaultPerTxAtomic: '100000000',
|
|
25
|
+
defaultDailyAtomic: '1000000000',
|
|
26
|
+
addressExplorerBaseUrl: 'https://solscan.io/account/',
|
|
27
|
+
transactionExplorerBaseUrl: 'https://solscan.io/tx/',
|
|
28
|
+
createDescription: 'Create a Solana wallet for agents that need SOL-native transfers and Solana ecosystem access.',
|
|
29
|
+
fundingInstructions: [
|
|
30
|
+
'Send SOL to this wallet address from any Solana wallet or exchange.',
|
|
31
|
+
'Make sure you are sending real SOL on Solana mainnet.',
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
ethereum: {
|
|
35
|
+
chain: 'ethereum',
|
|
36
|
+
label: 'Ethereum',
|
|
37
|
+
symbol: 'ETH',
|
|
38
|
+
decimals: 18,
|
|
39
|
+
defaultPerTxAtomic: '10000000000000000',
|
|
40
|
+
defaultDailyAtomic: '50000000000000000',
|
|
41
|
+
addressExplorerBaseUrl: 'https://etherscan.io/address/',
|
|
42
|
+
transactionExplorerBaseUrl: 'https://etherscan.io/tx/',
|
|
43
|
+
createDescription: 'Create an Ethereum-compatible EVM wallet for ETH-native transfers, exchange auth, and EVM ecosystem access.',
|
|
44
|
+
fundingInstructions: [
|
|
45
|
+
'Send ETH to this wallet address from any Ethereum-compatible wallet or exchange.',
|
|
46
|
+
'Make sure the sending network matches the wallet network you intend to use before transferring funds.',
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getWalletChainMeta(chain: WalletChain): WalletChainMeta {
|
|
52
|
+
return WALLET_CHAIN_META[chain] || WALLET_CHAIN_META.solana
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function normalizeWalletChainInput(value: unknown): WalletChain | null {
|
|
56
|
+
const normalized = String(value || '').trim().toLowerCase()
|
|
57
|
+
if (!normalized) return null
|
|
58
|
+
if (normalized === 'sol' || normalized === 'solana') return 'solana'
|
|
59
|
+
if (normalized === 'eth' || normalized === 'ethereum' || normalized === 'evm') {
|
|
60
|
+
return 'ethereum'
|
|
61
|
+
}
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getWalletChainOrDefault(value: unknown, fallback: WalletChain = 'solana'): WalletChain {
|
|
66
|
+
const normalized = normalizeWalletChainInput(value)
|
|
67
|
+
if (normalized) return normalized
|
|
68
|
+
const raw = String(value ?? '').trim()
|
|
69
|
+
if (raw) {
|
|
70
|
+
throw new Error(`Unsupported wallet chain or provider: ${raw}`)
|
|
71
|
+
}
|
|
72
|
+
return fallback
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getWalletDefaultLimitAtomic(chain: WalletChain, limit: 'perTx' | 'daily'): string {
|
|
76
|
+
const meta = getWalletChainMeta(chain)
|
|
77
|
+
return limit === 'perTx' ? meta.defaultPerTxAtomic : meta.defaultDailyAtomic
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function normalizeAtomicString(value: unknown, fallback = '0'): string {
|
|
81
|
+
if (typeof value === 'bigint') return value >= BigInt(0) ? value.toString() : fallback
|
|
82
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) return Math.floor(value).toString()
|
|
83
|
+
if (typeof value === 'string') {
|
|
84
|
+
const trimmed = value.trim()
|
|
85
|
+
if (/^\d+$/.test(trimmed)) return trimmed.replace(/^0+(?=\d)/, '') || '0'
|
|
86
|
+
}
|
|
87
|
+
return fallback
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function parseDisplayAmountToAtomic(value: string | number, decimals: number): string {
|
|
91
|
+
const raw = typeof value === 'number'
|
|
92
|
+
? value.toLocaleString('en-US', {
|
|
93
|
+
useGrouping: false,
|
|
94
|
+
maximumFractionDigits: decimals,
|
|
95
|
+
}).trim()
|
|
96
|
+
: String(value ?? '').trim()
|
|
97
|
+
if (!raw) throw new Error('Amount is required')
|
|
98
|
+
if (!/^\d+(\.\d+)?$/.test(raw)) throw new Error('Amount must be a positive decimal number')
|
|
99
|
+
|
|
100
|
+
const [whole, fraction = ''] = raw.split('.')
|
|
101
|
+
if (fraction.length > decimals) {
|
|
102
|
+
throw new Error(`Amount supports up to ${decimals} decimal places`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const wholePart = BigInt(whole || '0')
|
|
106
|
+
const fractionPadded = `${fraction}${'0'.repeat(decimals)}`.slice(0, decimals)
|
|
107
|
+
const fractionPart = BigInt(fractionPadded || '0')
|
|
108
|
+
const scale = BigInt(10) ** BigInt(decimals)
|
|
109
|
+
return ((wholePart * scale) + fractionPart).toString()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function formatAtomicAmount(
|
|
113
|
+
atomicValue: string | number | bigint,
|
|
114
|
+
decimals: number,
|
|
115
|
+
opts?: { minFractionDigits?: number; maxFractionDigits?: number }
|
|
116
|
+
): string {
|
|
117
|
+
const atomic = BigInt(normalizeAtomicString(atomicValue, '0'))
|
|
118
|
+
const scale = BigInt(10) ** BigInt(decimals)
|
|
119
|
+
const whole = atomic / scale
|
|
120
|
+
const fraction = atomic % scale
|
|
121
|
+
const maxFractionDigits = Math.max(0, Math.min(decimals, opts?.maxFractionDigits ?? decimals))
|
|
122
|
+
const minFractionDigits = Math.max(0, Math.min(maxFractionDigits, opts?.minFractionDigits ?? 0))
|
|
123
|
+
|
|
124
|
+
if (maxFractionDigits === 0) return whole.toString()
|
|
125
|
+
|
|
126
|
+
let fractionText = fraction.toString().padStart(decimals, '0').slice(0, maxFractionDigits)
|
|
127
|
+
if (fractionText.length < minFractionDigits) fractionText = fractionText.padEnd(minFractionDigits, '0')
|
|
128
|
+
if (fractionText.length > minFractionDigits) fractionText = fractionText.replace(/0+$/, '')
|
|
129
|
+
if (fractionText.length < minFractionDigits) fractionText = fractionText.padEnd(minFractionDigits, '0')
|
|
130
|
+
|
|
131
|
+
return fractionText.length > 0 ? `${whole.toString()}.${fractionText}` : whole.toString()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function formatWalletAmount(
|
|
135
|
+
chain: WalletChain,
|
|
136
|
+
atomicValue: string | number | bigint,
|
|
137
|
+
opts?: { minFractionDigits?: number; maxFractionDigits?: number }
|
|
138
|
+
): string {
|
|
139
|
+
return formatAtomicAmount(atomicValue, getWalletChainMeta(chain).decimals, opts)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function getWalletExplorerUrl(chain: WalletChain, kind: 'address' | 'transaction', value: string): string {
|
|
143
|
+
const meta = getWalletChainMeta(chain)
|
|
144
|
+
const base = kind === 'address' ? meta.addressExplorerBaseUrl : meta.transactionExplorerBaseUrl
|
|
145
|
+
return `${base}${value}`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getWalletAssetSymbol(chain: WalletChain): string {
|
|
149
|
+
return getWalletChainMeta(chain).symbol
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function getWalletAtomicAmount(value: { amountAtomic?: string; amountLamports?: number } | null | undefined): string {
|
|
153
|
+
if (!value) return '0'
|
|
154
|
+
return normalizeAtomicString(value.amountAtomic, normalizeAtomicString(value.amountLamports, '0'))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function getWalletFeeAtomicAmount(value: { feeAtomic?: string; feeLamports?: number } | null | undefined): string {
|
|
158
|
+
if (!value) return '0'
|
|
159
|
+
return normalizeAtomicString(value.feeAtomic, normalizeAtomicString(value.feeLamports, '0'))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function getWalletLimitAtomic(
|
|
163
|
+
wallet: { chain: WalletChain; spendingLimitAtomic?: string; spendingLimitLamports?: number; dailyLimitAtomic?: string; dailyLimitLamports?: number },
|
|
164
|
+
limit: 'perTx' | 'daily',
|
|
165
|
+
): string {
|
|
166
|
+
if (limit === 'perTx') {
|
|
167
|
+
return normalizeAtomicString(
|
|
168
|
+
wallet.spendingLimitAtomic,
|
|
169
|
+
normalizeAtomicString(wallet.spendingLimitLamports, getWalletDefaultLimitAtomic(wallet.chain, 'perTx')),
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
return normalizeAtomicString(
|
|
173
|
+
wallet.dailyLimitAtomic,
|
|
174
|
+
normalizeAtomicString(wallet.dailyLimitLamports, getWalletDefaultLimitAtomic(wallet.chain, 'daily')),
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function getWalletBalanceAtomic(
|
|
179
|
+
wallet: { balanceAtomic?: string; balanceLamports?: number | null } | null | undefined,
|
|
180
|
+
): string {
|
|
181
|
+
if (!wallet) return '0'
|
|
182
|
+
return normalizeAtomicString(wallet.balanceAtomic, normalizeAtomicString(wallet.balanceLamports, '0'))
|
|
183
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { afterEach, describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { NextRequest } from 'next/server'
|
|
5
|
+
|
|
6
|
+
import { proxy } from './proxy'
|
|
7
|
+
|
|
8
|
+
const originalAccessKey = process.env.ACCESS_KEY
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
if (originalAccessKey === undefined) delete process.env.ACCESS_KEY
|
|
12
|
+
else process.env.ACCESS_KEY = originalAccessKey
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('proxy', () => {
|
|
16
|
+
it('keeps CORS headers on plugin-install auth failures for allowed origins', () => {
|
|
17
|
+
process.env.ACCESS_KEY = 'top-secret'
|
|
18
|
+
|
|
19
|
+
const request = new NextRequest('http://localhost/api/plugins/install', {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: {
|
|
22
|
+
origin: 'https://swarmclaw.ai',
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const response = proxy(request)
|
|
27
|
+
assert.equal(response.status, 401)
|
|
28
|
+
assert.equal(response.headers.get('access-control-allow-origin'), 'https://swarmclaw.ai')
|
|
29
|
+
assert.equal(response.headers.get('vary'), 'Origin')
|
|
30
|
+
})
|
|
31
|
+
})
|