@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,337 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { genId } from '@/lib/id'
|
|
4
|
+
import { computeTaskFingerprint } from '@/lib/task-dedupe'
|
|
5
|
+
import { formatZodError } from '@/lib/validation/schemas'
|
|
6
|
+
import { loadSettings, loadTasks, logActivity, upsertStoredItems } from '@/lib/server/storage'
|
|
7
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
8
|
+
import type { BoardTask } from '@/types'
|
|
9
|
+
|
|
10
|
+
const MAX_IMPORT_LIMIT = 200
|
|
11
|
+
const BODY_CHAR_LIMIT = 12_000
|
|
12
|
+
|
|
13
|
+
const GitHubIssueImportSchema = z.object({
|
|
14
|
+
repo: z.string().trim().min(1, 'Repository is required'),
|
|
15
|
+
token: z.string().trim().optional().default(''),
|
|
16
|
+
state: z.enum(['open', 'closed', 'all']).optional().default('open'),
|
|
17
|
+
limit: z.coerce.number().int().min(1).max(MAX_IMPORT_LIMIT).optional().default(25),
|
|
18
|
+
labels: z.array(z.string()).optional().default([]),
|
|
19
|
+
projectId: z.string().trim().nullable().optional().default(null),
|
|
20
|
+
agentId: z.string().trim().nullable().optional().default(null),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
type GitHubIssueLabel = string | { name?: string | null }
|
|
24
|
+
|
|
25
|
+
interface GitHubIssueRecord {
|
|
26
|
+
id: number | string
|
|
27
|
+
number: number
|
|
28
|
+
title: string
|
|
29
|
+
body?: string | null
|
|
30
|
+
state?: string | null
|
|
31
|
+
html_url?: string | null
|
|
32
|
+
labels?: GitHubIssueLabel[]
|
|
33
|
+
assignee?: { login?: string | null } | null
|
|
34
|
+
user?: { login?: string | null } | null
|
|
35
|
+
pull_request?: unknown
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ParsedRepo {
|
|
39
|
+
owner: string
|
|
40
|
+
repo: string
|
|
41
|
+
fullName: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getGitHubToken(explicitToken: string): string {
|
|
45
|
+
return explicitToken.trim()
|
|
46
|
+
|| process.env.GITHUB_TOKEN
|
|
47
|
+
|| process.env.GH_TOKEN
|
|
48
|
+
|| process.env.GITHUB_PERSONAL_ACCESS_TOKEN
|
|
49
|
+
|| ''
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeLabelName(label: GitHubIssueLabel): string {
|
|
53
|
+
if (typeof label === 'string') return label.trim()
|
|
54
|
+
return String(label?.name || '').trim()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeTag(value: string): string {
|
|
58
|
+
return value.trim().replace(/\s+/g, ' ').slice(0, 60)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toIssueSummary(issue: GitHubIssueRecord, taskId?: string) {
|
|
62
|
+
return {
|
|
63
|
+
taskId,
|
|
64
|
+
number: issue.number,
|
|
65
|
+
title: issue.title || `Issue ${issue.number}`,
|
|
66
|
+
url: issue.html_url || null,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function parseGitHubRepoInput(input: string): ParsedRepo | null {
|
|
71
|
+
const trimmed = input.trim().replace(/\.git$/i, '')
|
|
72
|
+
if (!trimmed) return null
|
|
73
|
+
|
|
74
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
75
|
+
try {
|
|
76
|
+
const url = new URL(trimmed)
|
|
77
|
+
if (!/github\.com$/i.test(url.hostname)) return null
|
|
78
|
+
const parts = url.pathname.split('/').filter(Boolean)
|
|
79
|
+
if (parts.length < 2) return null
|
|
80
|
+
const owner = parts[0]
|
|
81
|
+
const repo = parts[1].replace(/\.git$/i, '')
|
|
82
|
+
if (!owner || !repo) return null
|
|
83
|
+
return { owner, repo, fullName: `${owner}/${repo}` }
|
|
84
|
+
} catch {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const compact = trimmed.replace(/^github\.com\//i, '')
|
|
90
|
+
const parts = compact.split('/').filter(Boolean)
|
|
91
|
+
if (parts.length < 2) return null
|
|
92
|
+
const owner = parts[0]
|
|
93
|
+
const repo = parts[1].replace(/\.git$/i, '')
|
|
94
|
+
if (!owner || !repo) return null
|
|
95
|
+
return { owner, repo, fullName: `${owner}/${repo}` }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildGitHubIssueTaskTitle(issue: GitHubIssueRecord, repoFullName: string): string {
|
|
99
|
+
const title = issue.title?.trim() || `Issue ${issue.number}`
|
|
100
|
+
return `[${repoFullName}#${issue.number}] ${title}`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function buildGitHubIssueTaskDescription(issue: GitHubIssueRecord, repoFullName: string): string {
|
|
104
|
+
const labels = (issue.labels || [])
|
|
105
|
+
.map(normalizeLabelName)
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
const header = [
|
|
108
|
+
`Imported from GitHub issue ${repoFullName}#${issue.number}`,
|
|
109
|
+
issue.html_url ? `URL: ${issue.html_url}` : '',
|
|
110
|
+
issue.state ? `State: ${issue.state}` : '',
|
|
111
|
+
labels.length > 0 ? `Labels: ${labels.join(', ')}` : '',
|
|
112
|
+
issue.assignee?.login ? `Assignee: ${issue.assignee.login}` : '',
|
|
113
|
+
issue.user?.login ? `Opened by: ${issue.user.login}` : '',
|
|
114
|
+
]
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.join('\n')
|
|
117
|
+
|
|
118
|
+
const rawBody = String(issue.body || '').trim()
|
|
119
|
+
if (!rawBody) return header
|
|
120
|
+
|
|
121
|
+
const body = rawBody.length > BODY_CHAR_LIMIT
|
|
122
|
+
? `${rawBody.slice(0, BODY_CHAR_LIMIT).trimEnd()}\n\n[Truncated during import]`
|
|
123
|
+
: rawBody
|
|
124
|
+
|
|
125
|
+
return `${header}\n\n${body}`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function buildGitHubIssueTaskTags(issue: GitHubIssueRecord, repoFullName: string): string[] {
|
|
129
|
+
const raw = [
|
|
130
|
+
'github',
|
|
131
|
+
repoFullName,
|
|
132
|
+
...(issue.labels || []).map(normalizeLabelName),
|
|
133
|
+
]
|
|
134
|
+
return Array.from(new Set(raw.map(normalizeTag).filter(Boolean))).slice(0, 8)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function findExistingImportedTask(
|
|
138
|
+
tasks: Record<string, BoardTask>,
|
|
139
|
+
repoFullName: string,
|
|
140
|
+
issueNumber: number,
|
|
141
|
+
): BoardTask | null {
|
|
142
|
+
for (const task of Object.values(tasks)) {
|
|
143
|
+
if (task.sourceType !== 'import') continue
|
|
144
|
+
if (task.externalSource?.source !== 'github') continue
|
|
145
|
+
if (task.externalSource?.repo !== repoFullName) continue
|
|
146
|
+
if (task.externalSource?.number !== issueNumber) continue
|
|
147
|
+
return task
|
|
148
|
+
}
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function fetchGitHubIssues(args: {
|
|
153
|
+
owner: string
|
|
154
|
+
repo: string
|
|
155
|
+
state: 'open' | 'closed' | 'all'
|
|
156
|
+
limit: number
|
|
157
|
+
labels: string[]
|
|
158
|
+
token: string
|
|
159
|
+
}): Promise<GitHubIssueRecord[]> {
|
|
160
|
+
const headers: Record<string, string> = {
|
|
161
|
+
Accept: 'application/vnd.github+json',
|
|
162
|
+
'User-Agent': 'SwarmClaw',
|
|
163
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
164
|
+
}
|
|
165
|
+
if (args.token) headers.Authorization = `Bearer ${args.token}`
|
|
166
|
+
|
|
167
|
+
const results: GitHubIssueRecord[] = []
|
|
168
|
+
const perPage = Math.min(100, Math.max(30, args.limit))
|
|
169
|
+
const maxPages = Math.max(1, Math.ceil(args.limit / 100) + 2)
|
|
170
|
+
|
|
171
|
+
for (let page = 1; page <= maxPages && results.length < args.limit; page++) {
|
|
172
|
+
const url = new URL(`https://api.github.com/repos/${args.owner}/${args.repo}/issues`)
|
|
173
|
+
url.searchParams.set('state', args.state)
|
|
174
|
+
url.searchParams.set('per_page', String(perPage))
|
|
175
|
+
url.searchParams.set('page', String(page))
|
|
176
|
+
if (args.labels.length > 0) url.searchParams.set('labels', args.labels.join(','))
|
|
177
|
+
|
|
178
|
+
const response = await fetch(url, {
|
|
179
|
+
headers,
|
|
180
|
+
cache: 'no-store',
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
const payload = await response.json().catch(() => null) as { message?: unknown } | null
|
|
185
|
+
const message = typeof payload?.message === 'string'
|
|
186
|
+
? payload.message
|
|
187
|
+
: `GitHub request failed (${response.status})`
|
|
188
|
+
const err = new Error(message) as Error & { status?: number }
|
|
189
|
+
err.status = response.status
|
|
190
|
+
throw err
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const payload = await response.json().catch(() => null) as unknown
|
|
194
|
+
if (!Array.isArray(payload)) {
|
|
195
|
+
throw new Error('GitHub returned an unexpected response.')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const pageIssues = payload
|
|
199
|
+
.filter((entry): entry is GitHubIssueRecord => !!entry && typeof entry === 'object')
|
|
200
|
+
.filter((entry) => !entry.pull_request)
|
|
201
|
+
|
|
202
|
+
results.push(...pageIssues)
|
|
203
|
+
if (payload.length < perPage) break
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return results.slice(0, args.limit)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function POST(req: Request) {
|
|
210
|
+
const raw = await req.json().catch(() => null)
|
|
211
|
+
const parsed = GitHubIssueImportSchema.safeParse(raw)
|
|
212
|
+
if (!parsed.success) {
|
|
213
|
+
return NextResponse.json(formatZodError(parsed.error), { status: 400 })
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const repo = parseGitHubRepoInput(parsed.data.repo)
|
|
217
|
+
if (!repo) {
|
|
218
|
+
return NextResponse.json({ error: 'Use a GitHub repo like owner/repo or a github.com URL.' }, { status: 400 })
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const labels = parsed.data.labels
|
|
222
|
+
.map((value) => String(value || '').trim())
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
|
|
225
|
+
let issues: GitHubIssueRecord[]
|
|
226
|
+
try {
|
|
227
|
+
issues = await fetchGitHubIssues({
|
|
228
|
+
owner: repo.owner,
|
|
229
|
+
repo: repo.repo,
|
|
230
|
+
state: parsed.data.state,
|
|
231
|
+
limit: parsed.data.limit,
|
|
232
|
+
labels,
|
|
233
|
+
token: getGitHubToken(parsed.data.token),
|
|
234
|
+
})
|
|
235
|
+
} catch (err) {
|
|
236
|
+
const message = err instanceof Error ? err.message : 'GitHub import failed.'
|
|
237
|
+
const status = typeof (err as { status?: unknown })?.status === 'number'
|
|
238
|
+
? Number((err as { status?: number }).status)
|
|
239
|
+
: 500
|
|
240
|
+
const responseStatus = [400, 401, 403, 404, 429].includes(status) ? status : 502
|
|
241
|
+
return NextResponse.json({ error: message }, { status: responseStatus })
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const tasks = loadTasks() as Record<string, BoardTask>
|
|
245
|
+
const settings = loadSettings()
|
|
246
|
+
const now = Date.now()
|
|
247
|
+
const maxAttempts = Math.max(1, Math.min(20, Math.trunc(Number(settings.defaultTaskMaxAttempts ?? 3))))
|
|
248
|
+
const retryBackoffSec = Math.max(1, Math.min(3600, Math.trunc(Number(settings.taskRetryBackoffSec ?? 30))))
|
|
249
|
+
const projectId = parsed.data.projectId || undefined
|
|
250
|
+
const agentId = parsed.data.agentId || ''
|
|
251
|
+
|
|
252
|
+
const created: Array<ReturnType<typeof toIssueSummary>> = []
|
|
253
|
+
const skipped: Array<ReturnType<typeof toIssueSummary>> = []
|
|
254
|
+
const taskEntries: Array<[string, BoardTask]> = []
|
|
255
|
+
|
|
256
|
+
for (const issue of issues) {
|
|
257
|
+
const existing = findExistingImportedTask(tasks, repo.fullName, issue.number)
|
|
258
|
+
if (existing) {
|
|
259
|
+
skipped.push(toIssueSummary(issue, existing.id))
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const id = genId()
|
|
264
|
+
const title = buildGitHubIssueTaskTitle(issue, repo.fullName)
|
|
265
|
+
const task: BoardTask = {
|
|
266
|
+
id,
|
|
267
|
+
title,
|
|
268
|
+
description: buildGitHubIssueTaskDescription(issue, repo.fullName),
|
|
269
|
+
status: 'backlog',
|
|
270
|
+
agentId,
|
|
271
|
+
projectId,
|
|
272
|
+
result: null,
|
|
273
|
+
error: null,
|
|
274
|
+
outputFiles: [],
|
|
275
|
+
artifacts: [],
|
|
276
|
+
createdAt: now,
|
|
277
|
+
updatedAt: now,
|
|
278
|
+
queuedAt: null,
|
|
279
|
+
startedAt: null,
|
|
280
|
+
completedAt: null,
|
|
281
|
+
archivedAt: null,
|
|
282
|
+
attempts: 0,
|
|
283
|
+
maxAttempts,
|
|
284
|
+
retryBackoffSec,
|
|
285
|
+
retryScheduledAt: null,
|
|
286
|
+
deadLetteredAt: null,
|
|
287
|
+
checkpoint: null,
|
|
288
|
+
blockedBy: [],
|
|
289
|
+
blocks: [],
|
|
290
|
+
tags: buildGitHubIssueTaskTags(issue, repo.fullName),
|
|
291
|
+
sourceType: 'import',
|
|
292
|
+
externalSource: {
|
|
293
|
+
source: 'github',
|
|
294
|
+
id: String(issue.id),
|
|
295
|
+
repo: repo.fullName,
|
|
296
|
+
number: issue.number,
|
|
297
|
+
state: issue.state || null,
|
|
298
|
+
labels: (issue.labels || []).map(normalizeLabelName).filter(Boolean),
|
|
299
|
+
assignee: issue.assignee?.login || null,
|
|
300
|
+
url: issue.html_url || null,
|
|
301
|
+
},
|
|
302
|
+
fingerprint: computeTaskFingerprint(title, agentId),
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
tasks[id] = task
|
|
306
|
+
taskEntries.push([id, task])
|
|
307
|
+
created.push(toIssueSummary(issue, id))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (taskEntries.length > 0) {
|
|
311
|
+
upsertStoredItems('tasks', taskEntries)
|
|
312
|
+
notify('tasks')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
logActivity({
|
|
316
|
+
entityType: 'task',
|
|
317
|
+
entityId: created[0]?.taskId || `github:${repo.fullName}`,
|
|
318
|
+
action: 'imported',
|
|
319
|
+
actor: 'user',
|
|
320
|
+
summary: `GitHub import from ${repo.fullName}: ${created.length} created, ${skipped.length} skipped`,
|
|
321
|
+
detail: {
|
|
322
|
+
repo: repo.fullName,
|
|
323
|
+
state: parsed.data.state,
|
|
324
|
+
labels,
|
|
325
|
+
created: created.length,
|
|
326
|
+
skipped: skipped.length,
|
|
327
|
+
},
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
return NextResponse.json({
|
|
331
|
+
repo: repo.fullName,
|
|
332
|
+
state: parsed.data.state,
|
|
333
|
+
fetched: issues.length,
|
|
334
|
+
created,
|
|
335
|
+
skipped,
|
|
336
|
+
})
|
|
337
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { loadWallets, loadWalletTransactions, upsertWalletTransaction } from '@/lib/server/storage'
|
|
3
|
-
import { sendSol } from '@/lib/server/solana'
|
|
4
3
|
import { notify } from '@/lib/server/ws-hub'
|
|
5
4
|
import type { AgentWallet, WalletTransaction } from '@/types'
|
|
5
|
+
import { getWalletAtomicAmount } from '@/lib/wallet'
|
|
6
|
+
import { sendWalletNativeAsset, validateWalletSendLimits } from '@/lib/server/wallet-service'
|
|
6
7
|
export const dynamic = 'force-dynamic'
|
|
7
8
|
|
|
8
9
|
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
@@ -41,10 +42,23 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
41
42
|
|
|
42
43
|
// Approve — sign and submit
|
|
43
44
|
try {
|
|
44
|
-
const
|
|
45
|
+
const limitError = validateWalletSendLimits({ wallet, amountAtomic: getWalletAtomicAmount(tx), excludeTransactionId: transactionId })
|
|
46
|
+
if (limitError) {
|
|
47
|
+
tx.status = 'failed'
|
|
48
|
+
upsertWalletTransaction(transactionId, tx)
|
|
49
|
+
notify('wallets')
|
|
50
|
+
return NextResponse.json({
|
|
51
|
+
error: limitError,
|
|
52
|
+
transactionId,
|
|
53
|
+
status: 'failed',
|
|
54
|
+
}, { status: limitError === 'Amount must be positive' ? 400 : 403 })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { signature, feeAtomic } = await sendWalletNativeAsset(wallet, tx.toAddress, getWalletAtomicAmount(tx))
|
|
45
58
|
tx.status = 'confirmed'
|
|
46
59
|
tx.signature = signature
|
|
47
|
-
tx.
|
|
60
|
+
tx.feeAtomic = feeAtomic
|
|
61
|
+
tx.feeLamports = wallet.chain === 'solana' && feeAtomic ? Number.parseInt(feeAtomic, 10) : undefined
|
|
48
62
|
tx.approvedBy = 'user'
|
|
49
63
|
upsertWalletTransaction(transactionId, tx)
|
|
50
64
|
notify('wallets')
|
|
@@ -1,12 +1,46 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { loadWallets, upsertWallet, deleteWallet as deleteWalletFromStore, loadAgents, saveAgents } from '@/lib/server/storage'
|
|
3
|
-
import { getBalance, lamportsToSol } from '@/lib/server/solana'
|
|
4
3
|
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
-
import
|
|
4
|
+
import { getWalletLimitAtomic, normalizeAtomicString } from '@/lib/wallet'
|
|
5
|
+
import type { AgentWallet, WalletAssetBalance, WalletPortfolioSummary } from '@/types'
|
|
6
|
+
import { buildEmptyWalletPortfolio } from '@/lib/server/wallet-portfolio'
|
|
7
|
+
import {
|
|
8
|
+
getAgentActiveWalletId,
|
|
9
|
+
getWalletPortfolioSnapshot,
|
|
10
|
+
linkWalletToAgent,
|
|
11
|
+
setAgentActiveWallet,
|
|
12
|
+
stripWalletPrivateKey,
|
|
13
|
+
unlinkWalletFromAgent,
|
|
14
|
+
} from '@/lib/server/wallet-service'
|
|
6
15
|
export const dynamic = 'force-dynamic'
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
16
|
+
const WALLET_DETAIL_PORTFOLIO_TIMEOUT_MS = 2500
|
|
17
|
+
|
|
18
|
+
function withPortfolio(
|
|
19
|
+
wallet: AgentWallet,
|
|
20
|
+
portfolio: {
|
|
21
|
+
balanceAtomic: string
|
|
22
|
+
balanceFormatted: string
|
|
23
|
+
balanceSymbol: string
|
|
24
|
+
balanceDisplay: string
|
|
25
|
+
balanceLamports?: number
|
|
26
|
+
balanceSol?: number
|
|
27
|
+
assets: WalletAssetBalance[]
|
|
28
|
+
summary: WalletPortfolioSummary
|
|
29
|
+
},
|
|
30
|
+
isActive: boolean,
|
|
31
|
+
) {
|
|
32
|
+
return {
|
|
33
|
+
...stripWalletPrivateKey(wallet as unknown as Record<string, unknown>),
|
|
34
|
+
balanceAtomic: portfolio.balanceAtomic,
|
|
35
|
+
balanceFormatted: portfolio.balanceFormatted,
|
|
36
|
+
balanceSymbol: portfolio.balanceSymbol,
|
|
37
|
+
balanceDisplay: portfolio.balanceDisplay,
|
|
38
|
+
balanceLamports: portfolio.balanceLamports,
|
|
39
|
+
balanceSol: portfolio.balanceSol,
|
|
40
|
+
assets: portfolio.assets,
|
|
41
|
+
portfolioSummary: portfolio.summary,
|
|
42
|
+
isActive,
|
|
43
|
+
}
|
|
10
44
|
}
|
|
11
45
|
|
|
12
46
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
@@ -15,21 +49,19 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
15
49
|
const wallet = wallets[id]
|
|
16
50
|
if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
|
|
17
51
|
|
|
18
|
-
|
|
19
|
-
let balanceLamports = 0
|
|
20
|
-
let balanceSol = 0
|
|
52
|
+
let portfolio = buildEmptyWalletPortfolio(wallet)
|
|
21
53
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
54
|
+
portfolio = await getWalletPortfolioSnapshot(wallet, {
|
|
55
|
+
timeoutMs: WALLET_DETAIL_PORTFOLIO_TIMEOUT_MS,
|
|
56
|
+
allowStale: true,
|
|
57
|
+
})
|
|
24
58
|
} catch {
|
|
25
59
|
// RPC failure — return 0
|
|
26
60
|
}
|
|
27
61
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
balanceSol,
|
|
32
|
-
})
|
|
62
|
+
const agents = loadAgents()
|
|
63
|
+
const isActive = getAgentActiveWalletId(agents[wallet.agentId]) === wallet.id
|
|
64
|
+
return NextResponse.json(withPortfolio(wallet, portfolio, isActive))
|
|
33
65
|
}
|
|
34
66
|
|
|
35
67
|
export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
@@ -39,6 +71,7 @@ export async function PATCH(req: Request, { params }: { params: Promise<{ id: st
|
|
|
39
71
|
if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
|
|
40
72
|
|
|
41
73
|
const body = await req.json()
|
|
74
|
+
const shouldMakeActive = body.makeActive === true
|
|
42
75
|
|
|
43
76
|
// Reassign wallet to a different agent
|
|
44
77
|
if (typeof body.agentId === 'string' && body.agentId !== wallet.agentId) {
|
|
@@ -46,39 +79,51 @@ export async function PATCH(req: Request, { params }: { params: Promise<{ id: st
|
|
|
46
79
|
const newAgent = agents[body.agentId]
|
|
47
80
|
if (!newAgent) return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
|
48
81
|
|
|
49
|
-
//
|
|
82
|
+
// Only one wallet per chain per agent.
|
|
50
83
|
const allWallets = loadWallets() as Record<string, AgentWallet>
|
|
51
|
-
const conflict = Object.values(allWallets).find((w) => w.agentId === body.agentId && w.id !== id)
|
|
52
|
-
if (conflict) return NextResponse.json({ error:
|
|
84
|
+
const conflict = Object.values(allWallets).find((w) => w.agentId === body.agentId && w.id !== id && w.chain === wallet.chain)
|
|
85
|
+
if (conflict) return NextResponse.json({ error: `Target agent already has a ${wallet.chain} wallet` }, { status: 409 })
|
|
53
86
|
|
|
54
|
-
// Unlink old agent
|
|
55
87
|
const oldAgent = agents[wallet.agentId]
|
|
56
88
|
if (oldAgent) {
|
|
57
|
-
oldAgent
|
|
89
|
+
unlinkWalletFromAgent(oldAgent, id)
|
|
58
90
|
oldAgent.updatedAt = Date.now()
|
|
59
91
|
agents[wallet.agentId] = oldAgent
|
|
60
92
|
}
|
|
61
93
|
|
|
62
|
-
|
|
63
|
-
newAgent.walletId = id
|
|
94
|
+
linkWalletToAgent(newAgent, id, shouldMakeActive || getAgentActiveWalletId(newAgent) == null)
|
|
64
95
|
newAgent.updatedAt = Date.now()
|
|
65
96
|
agents[body.agentId] = newAgent
|
|
66
97
|
saveAgents(agents)
|
|
67
98
|
notify('agents')
|
|
68
99
|
|
|
69
100
|
wallet.agentId = body.agentId
|
|
101
|
+
} else if (shouldMakeActive) {
|
|
102
|
+
const agents = loadAgents()
|
|
103
|
+
const agent = agents[wallet.agentId]
|
|
104
|
+
if (agent) {
|
|
105
|
+
setAgentActiveWallet(agent, id)
|
|
106
|
+
agent.updatedAt = Date.now()
|
|
107
|
+
agents[wallet.agentId] = agent
|
|
108
|
+
saveAgents(agents)
|
|
109
|
+
notify('agents')
|
|
110
|
+
}
|
|
70
111
|
}
|
|
71
112
|
|
|
72
113
|
if (body.label !== undefined) wallet.label = body.label
|
|
73
|
-
if (
|
|
74
|
-
|
|
114
|
+
if (body.spendingLimitAtomic !== undefined || body.spendingLimitLamports !== undefined) {
|
|
115
|
+
wallet.spendingLimitAtomic = normalizeAtomicString(body.spendingLimitAtomic ?? body.spendingLimitLamports, getWalletLimitAtomic(wallet, 'perTx'))
|
|
116
|
+
}
|
|
117
|
+
if (body.dailyLimitAtomic !== undefined || body.dailyLimitLamports !== undefined) {
|
|
118
|
+
wallet.dailyLimitAtomic = normalizeAtomicString(body.dailyLimitAtomic ?? body.dailyLimitLamports, getWalletLimitAtomic(wallet, 'daily'))
|
|
119
|
+
}
|
|
75
120
|
if (typeof body.requireApproval === 'boolean') wallet.requireApproval = body.requireApproval
|
|
76
121
|
wallet.updatedAt = Date.now()
|
|
77
122
|
|
|
78
123
|
upsertWallet(id, wallet)
|
|
79
124
|
notify('wallets')
|
|
80
125
|
|
|
81
|
-
return NextResponse.json(
|
|
126
|
+
return NextResponse.json(stripWalletPrivateKey(wallet as unknown as Record<string, unknown>))
|
|
82
127
|
}
|
|
83
128
|
|
|
84
129
|
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
@@ -88,20 +133,19 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
|
|
|
88
133
|
if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
|
|
89
134
|
|
|
90
135
|
// Check if balance > 0 and warn
|
|
91
|
-
let
|
|
136
|
+
let portfolio = buildEmptyWalletPortfolio(wallet)
|
|
92
137
|
try {
|
|
93
|
-
|
|
138
|
+
portfolio = await getWalletPortfolioSnapshot(wallet, {
|
|
139
|
+
timeoutMs: WALLET_DETAIL_PORTFOLIO_TIMEOUT_MS,
|
|
140
|
+
allowStale: true,
|
|
141
|
+
})
|
|
94
142
|
} catch { /* ignore */ }
|
|
95
143
|
|
|
96
|
-
if (balanceLamports > 0) {
|
|
97
|
-
// Still delete, but include warning
|
|
98
|
-
}
|
|
99
|
-
|
|
100
144
|
// Unlink from agent
|
|
101
145
|
const agents = loadAgents()
|
|
102
146
|
const agent = agents[wallet.agentId]
|
|
103
147
|
if (agent) {
|
|
104
|
-
agent
|
|
148
|
+
unlinkWalletFromAgent(agent, id)
|
|
105
149
|
agent.updatedAt = Date.now()
|
|
106
150
|
agents[wallet.agentId] = agent
|
|
107
151
|
saveAgents(agents)
|
|
@@ -113,6 +157,8 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
|
|
|
113
157
|
|
|
114
158
|
return NextResponse.json({
|
|
115
159
|
ok: true,
|
|
116
|
-
warning:
|
|
160
|
+
warning: portfolio.summary.nonZeroAssets > 0
|
|
161
|
+
? `Wallet still had ${portfolio.summary.nonZeroAssets} asset${portfolio.summary.nonZeroAssets === 1 ? '' : 's'} remaining, including ${portfolio.balanceDisplay}`
|
|
162
|
+
: undefined,
|
|
117
163
|
})
|
|
118
164
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
|
-
import { loadWallets,
|
|
4
|
-
import { sendSol, isValidSolanaAddress, lamportsToSol } from '@/lib/server/solana'
|
|
3
|
+
import { loadWallets, upsertWalletTransaction } from '@/lib/server/storage'
|
|
5
4
|
import { notify } from '@/lib/server/ws-hub'
|
|
6
5
|
import type { AgentWallet, WalletTransaction } from '@/types'
|
|
6
|
+
import {
|
|
7
|
+
normalizeAtomicString,
|
|
8
|
+
} from '@/lib/wallet'
|
|
9
|
+
import { isValidWalletAddress, sendWalletNativeAsset, validateWalletSendLimits } from '@/lib/server/wallet-service'
|
|
7
10
|
export const dynamic = 'force-dynamic'
|
|
8
11
|
|
|
9
12
|
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
@@ -14,36 +17,15 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
14
17
|
|
|
15
18
|
const body = await req.json()
|
|
16
19
|
const toAddress = typeof body.toAddress === 'string' ? body.toAddress.trim() : ''
|
|
17
|
-
const
|
|
20
|
+
const amountAtomic = normalizeAtomicString(body.amountAtomic ?? body.amountLamports, '0')
|
|
18
21
|
const memo = typeof body.memo === 'string' ? body.memo.slice(0, 500) : undefined
|
|
19
22
|
|
|
20
|
-
if (!toAddress || !
|
|
23
|
+
if (!toAddress || !isValidWalletAddress(wallet.chain, toAddress)) {
|
|
21
24
|
return NextResponse.json({ error: 'Invalid recipient address' }, { status: 400 })
|
|
22
25
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// Per-tx spending limit
|
|
28
|
-
const perTxLimit = wallet.spendingLimitLamports ?? 100_000_000
|
|
29
|
-
if (amountLamports > perTxLimit) {
|
|
30
|
-
return NextResponse.json({
|
|
31
|
-
error: `Amount ${lamportsToSol(amountLamports)} SOL exceeds per-transaction limit of ${lamportsToSol(perTxLimit)} SOL`,
|
|
32
|
-
}, { status: 403 })
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// 24h rolling daily limit
|
|
36
|
-
const dailyLimit = wallet.dailyLimitLamports ?? 1_000_000_000
|
|
37
|
-
const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000
|
|
38
|
-
const allTxs = loadWalletTransactions() as Record<string, WalletTransaction>
|
|
39
|
-
const recentSends = Object.values(allTxs).filter(
|
|
40
|
-
(tx) => tx.walletId === id && tx.type === 'send' && tx.status === 'confirmed' && tx.timestamp > oneDayAgo,
|
|
41
|
-
)
|
|
42
|
-
const dailySpent = recentSends.reduce((sum, tx) => sum + tx.amountLamports, 0)
|
|
43
|
-
if (dailySpent + amountLamports > dailyLimit) {
|
|
44
|
-
return NextResponse.json({
|
|
45
|
-
error: `Daily limit exceeded. Spent ${lamportsToSol(dailySpent)} SOL in last 24h, limit is ${lamportsToSol(dailyLimit)} SOL`,
|
|
46
|
-
}, { status: 403 })
|
|
26
|
+
const limitError = validateWalletSendLimits({ wallet, amountAtomic })
|
|
27
|
+
if (limitError) {
|
|
28
|
+
return NextResponse.json({ error: limitError }, { status: limitError === 'Amount must be positive' ? 400 : 403 })
|
|
47
29
|
}
|
|
48
30
|
|
|
49
31
|
const txId = genId(8)
|
|
@@ -60,7 +42,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
60
42
|
signature: '',
|
|
61
43
|
fromAddress: wallet.publicKey,
|
|
62
44
|
toAddress,
|
|
63
|
-
|
|
45
|
+
amountAtomic,
|
|
46
|
+
amountLamports: wallet.chain === 'solana' ? Number.parseInt(amountAtomic, 10) : undefined,
|
|
64
47
|
status: 'pending_approval',
|
|
65
48
|
memo,
|
|
66
49
|
timestamp: now,
|
|
@@ -72,7 +55,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
72
55
|
|
|
73
56
|
// Auto-approved — sign and submit
|
|
74
57
|
try {
|
|
75
|
-
const { signature,
|
|
58
|
+
const { signature, feeAtomic } = await sendWalletNativeAsset(wallet, toAddress, amountAtomic)
|
|
76
59
|
const confirmedTx: WalletTransaction = {
|
|
77
60
|
id: txId,
|
|
78
61
|
walletId: id,
|
|
@@ -82,8 +65,10 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
82
65
|
signature,
|
|
83
66
|
fromAddress: wallet.publicKey,
|
|
84
67
|
toAddress,
|
|
85
|
-
|
|
86
|
-
|
|
68
|
+
amountAtomic,
|
|
69
|
+
amountLamports: wallet.chain === 'solana' ? Number.parseInt(amountAtomic, 10) : undefined,
|
|
70
|
+
feeAtomic,
|
|
71
|
+
feeLamports: wallet.chain === 'solana' && feeAtomic ? Number.parseInt(feeAtomic, 10) : undefined,
|
|
87
72
|
status: 'confirmed',
|
|
88
73
|
memo,
|
|
89
74
|
approvedBy: 'auto',
|
|
@@ -102,7 +87,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
102
87
|
signature: '',
|
|
103
88
|
fromAddress: wallet.publicKey,
|
|
104
89
|
toAddress,
|
|
105
|
-
|
|
90
|
+
amountAtomic,
|
|
91
|
+
amountLamports: wallet.chain === 'solana' ? Number.parseInt(amountAtomic, 10) : undefined,
|
|
106
92
|
status: 'failed',
|
|
107
93
|
memo,
|
|
108
94
|
timestamp: now,
|