@swarmclawai/swarmclaw 0.7.8 → 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 -15
- 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 +22 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +26 -1
- 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/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/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 +73 -24
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +44 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- 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/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/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 +7 -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 +191 -95
- package/src/components/tasks/task-board.tsx +273 -2
- package/src/components/tasks/task-card.tsx +38 -9
- 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 +11 -0
- package/src/lib/server/capability-router.ts +26 -1
- 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 +353 -72
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +362 -63
- 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 +1 -1
- 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 +189 -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 +15 -10
- 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/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 +2 -2
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +205 -5
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +262 -0
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +293 -61
- 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 +52 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +348 -61
- package/src/lib/server/session-tools/context.ts +12 -3
- package/src/lib/server/session-tools/crud.ts +221 -10
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate.ts +64 -8
- 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/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +546 -79
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- 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 +162 -1
- package/src/lib/server/session-tools/web.ts +468 -64
- 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 +419 -9
- package/src/lib/server/stream-agent-chat.ts +887 -83
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- 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.ts +4 -2
- 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-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 +210 -14
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { normalizeOllamaSetupEndpoint } from './route'
|
|
5
|
+
|
|
6
|
+
test('normalizeOllamaSetupEndpoint strips local /v1 suffixes but preserves cloud endpoints', () => {
|
|
7
|
+
assert.equal(
|
|
8
|
+
normalizeOllamaSetupEndpoint('http://localhost:11434/v1', false),
|
|
9
|
+
'http://localhost:11434',
|
|
10
|
+
)
|
|
11
|
+
assert.equal(
|
|
12
|
+
normalizeOllamaSetupEndpoint('http://localhost:11434/', false),
|
|
13
|
+
'http://localhost:11434',
|
|
14
|
+
)
|
|
15
|
+
assert.equal(
|
|
16
|
+
normalizeOllamaSetupEndpoint('https://ollama.com/v1', true),
|
|
17
|
+
'https://ollama.com/v1',
|
|
18
|
+
)
|
|
19
|
+
})
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { loadCredentials, decryptKey } from '@/lib/server/storage'
|
|
3
3
|
import { getDeviceId, wsConnect } from '@/lib/providers/openclaw'
|
|
4
4
|
import { OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
|
|
5
|
+
import { resolveOllamaRuntimeConfig } from '@/lib/server/ollama-runtime'
|
|
5
6
|
|
|
6
7
|
type SetupProvider =
|
|
7
8
|
| 'openai'
|
|
@@ -28,6 +29,12 @@ function clean(value: unknown): string {
|
|
|
28
29
|
return typeof value === 'string' ? value.trim() : ''
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
export function normalizeOllamaSetupEndpoint(endpoint: string, useCloud: boolean): string {
|
|
33
|
+
const normalized = endpoint.replace(/\/+$/, '')
|
|
34
|
+
if (useCloud) return normalized
|
|
35
|
+
return normalized.replace(/\/v1$/i, '')
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
function parseBody(input: unknown): SetupCheckBody {
|
|
32
39
|
if (!input || typeof input !== 'object' || Array.isArray(input)) return {}
|
|
33
40
|
return input as SetupCheckBody
|
|
@@ -101,9 +108,28 @@ async function checkAnthropic(apiKey: string, modelRaw: string): Promise<{ ok: b
|
|
|
101
108
|
return { ok: true, message: text ? `Connected to Anthropic. Sample: ${text.slice(0, 120)}` : 'Connected to Anthropic.' }
|
|
102
109
|
}
|
|
103
110
|
|
|
104
|
-
async function checkOllama(
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
async function checkOllama(params: {
|
|
112
|
+
endpointRaw: string
|
|
113
|
+
modelRaw: string
|
|
114
|
+
apiKey?: string
|
|
115
|
+
}): Promise<{ ok: boolean; message: string; normalizedEndpoint: string; recommendedModel?: string }> {
|
|
116
|
+
const runtime = resolveOllamaRuntimeConfig({
|
|
117
|
+
model: params.modelRaw,
|
|
118
|
+
apiKey: params.apiKey,
|
|
119
|
+
apiEndpoint: params.endpointRaw,
|
|
120
|
+
})
|
|
121
|
+
const normalizedEndpoint = normalizeOllamaSetupEndpoint(runtime.endpoint, runtime.useCloud)
|
|
122
|
+
const tagsPath = runtime.useCloud ? '/v1/models' : '/api/tags'
|
|
123
|
+
const headers = runtime.apiKey ? { authorization: `Bearer ${runtime.apiKey}` } : undefined
|
|
124
|
+
if (runtime.useCloud && !runtime.apiKey) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
message: 'Ollama Cloud model requires an API key. Set OLLAMA_API_KEY or attach an Ollama credential.',
|
|
128
|
+
normalizedEndpoint,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const res = await fetch(`${normalizedEndpoint}${tagsPath}`, {
|
|
132
|
+
headers,
|
|
107
133
|
signal: AbortSignal.timeout(8_000),
|
|
108
134
|
cache: 'no-store',
|
|
109
135
|
})
|
|
@@ -112,20 +138,24 @@ async function checkOllama(endpointRaw: string): Promise<{ ok: boolean; message:
|
|
|
112
138
|
return { ok: false, message: detail, normalizedEndpoint }
|
|
113
139
|
}
|
|
114
140
|
const payload = await res.json().catch(() => ({} as any))
|
|
115
|
-
const models =
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
141
|
+
const models = runtime.useCloud
|
|
142
|
+
? (Array.isArray(payload?.data) ? payload.data : [])
|
|
143
|
+
: (Array.isArray(payload?.models) ? payload.models : [])
|
|
144
|
+
const firstModel = runtime.useCloud
|
|
145
|
+
? (typeof models[0]?.id === 'string' ? String(models[0].id) : undefined)
|
|
146
|
+
: (typeof models[0]?.name === 'string' ? String(models[0].name).replace(/:latest$/, '') : undefined)
|
|
119
147
|
if (models.length === 0) {
|
|
120
148
|
return {
|
|
121
149
|
ok: true,
|
|
122
|
-
message:
|
|
150
|
+
message: runtime.useCloud
|
|
151
|
+
? 'Connected to Ollama Cloud, but no models were returned.'
|
|
152
|
+
: 'Connected to Ollama, but no models are installed yet. Run `ollama pull <model>` to add one.',
|
|
123
153
|
normalizedEndpoint,
|
|
124
154
|
}
|
|
125
155
|
}
|
|
126
156
|
return {
|
|
127
157
|
ok: true,
|
|
128
|
-
message: `Connected to Ollama. ${models.length} model(s) available.`,
|
|
158
|
+
message: `Connected to ${runtime.useCloud ? 'Ollama Cloud' : 'Ollama'}. ${models.length} model(s) available.`,
|
|
129
159
|
normalizedEndpoint,
|
|
130
160
|
recommendedModel: firstModel,
|
|
131
161
|
}
|
|
@@ -205,7 +235,7 @@ export async function POST(req: Request) {
|
|
|
205
235
|
return NextResponse.json(result)
|
|
206
236
|
}
|
|
207
237
|
case 'ollama': {
|
|
208
|
-
const result = await checkOllama(endpoint)
|
|
238
|
+
const result = await checkOllama({ endpointRaw: endpoint, modelRaw: model, apiKey })
|
|
209
239
|
return NextResponse.json(result)
|
|
210
240
|
}
|
|
211
241
|
case 'openclaw': {
|
|
@@ -31,6 +31,18 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
31
31
|
content: normalized.content,
|
|
32
32
|
sourceUrl: normalized.sourceUrl,
|
|
33
33
|
sourceFormat: normalized.sourceFormat,
|
|
34
|
+
author: normalized.author ?? skill.author,
|
|
35
|
+
tags: normalized.tags ?? skill.tags,
|
|
36
|
+
version: normalized.version ?? null,
|
|
37
|
+
homepage: normalized.homepage ?? null,
|
|
38
|
+
primaryEnv: normalized.primaryEnv ?? null,
|
|
39
|
+
skillKey: normalized.skillKey ?? null,
|
|
40
|
+
always: typeof normalized.always === 'boolean' ? normalized.always : false,
|
|
41
|
+
installOptions: normalized.installOptions,
|
|
42
|
+
skillRequirements: normalized.skillRequirements,
|
|
43
|
+
detectedEnvVars: normalized.detectedEnvVars,
|
|
44
|
+
security: normalized.security,
|
|
45
|
+
frontmatter: normalized.frontmatter,
|
|
34
46
|
scope: updatedScope,
|
|
35
47
|
agentIds: updatedAgentIds,
|
|
36
48
|
id,
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { genId } from '@/lib/id'
|
|
2
1
|
import { NextResponse } from 'next/server'
|
|
3
|
-
import { loadSkills, saveSkills } from '@/lib/server/storage'
|
|
4
2
|
import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
|
|
5
3
|
|
|
6
4
|
const MAX_SKILL_BYTES = 2 * 1024 * 1024
|
|
@@ -46,22 +44,26 @@ export async function POST(req: Request) {
|
|
|
46
44
|
sourceUrl: url,
|
|
47
45
|
})
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
const id = genId()
|
|
51
|
-
skills[id] = {
|
|
52
|
-
id,
|
|
47
|
+
return NextResponse.json({
|
|
53
48
|
name: normalized.name,
|
|
54
49
|
filename: normalized.filename,
|
|
55
50
|
description: normalized.description,
|
|
56
51
|
content: normalized.content,
|
|
57
52
|
sourceUrl: normalized.sourceUrl,
|
|
58
53
|
sourceFormat: normalized.sourceFormat,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
author: normalized.author,
|
|
55
|
+
tags: normalized.tags,
|
|
56
|
+
version: normalized.version,
|
|
57
|
+
homepage: normalized.homepage,
|
|
58
|
+
primaryEnv: normalized.primaryEnv,
|
|
59
|
+
skillKey: normalized.skillKey,
|
|
60
|
+
always: normalized.always,
|
|
61
|
+
installOptions: normalized.installOptions,
|
|
62
|
+
skillRequirements: normalized.skillRequirements,
|
|
63
|
+
detectedEnvVars: normalized.detectedEnvVars,
|
|
64
|
+
security: normalized.security,
|
|
65
|
+
frontmatter: normalized.frontmatter,
|
|
66
|
+
})
|
|
65
67
|
} catch (err: unknown) {
|
|
66
68
|
const message = err instanceof Error ? err.message : 'Failed to import skill'
|
|
67
69
|
return NextResponse.json({ error: message }, { status: 400 })
|
|
@@ -5,7 +5,7 @@ import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
|
|
|
5
5
|
export const dynamic = 'force-dynamic'
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
export async function GET(
|
|
8
|
+
export async function GET() {
|
|
9
9
|
return NextResponse.json(loadSkills())
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -26,6 +26,18 @@ export async function POST(req: Request) {
|
|
|
26
26
|
description: normalized.description || '',
|
|
27
27
|
sourceUrl: normalized.sourceUrl,
|
|
28
28
|
sourceFormat: normalized.sourceFormat,
|
|
29
|
+
author: normalized.author,
|
|
30
|
+
tags: normalized.tags,
|
|
31
|
+
version: normalized.version,
|
|
32
|
+
homepage: normalized.homepage,
|
|
33
|
+
primaryEnv: normalized.primaryEnv,
|
|
34
|
+
skillKey: normalized.skillKey,
|
|
35
|
+
always: normalized.always,
|
|
36
|
+
installOptions: normalized.installOptions,
|
|
37
|
+
skillRequirements: normalized.skillRequirements,
|
|
38
|
+
detectedEnvVars: normalized.detectedEnvVars,
|
|
39
|
+
security: normalized.security,
|
|
40
|
+
frontmatter: normalized.frontmatter,
|
|
29
41
|
scope,
|
|
30
42
|
agentIds,
|
|
31
43
|
createdAt: Date.now(),
|
|
@@ -126,7 +126,16 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
126
126
|
enqueueSystemEvent(tasks[id].sessionId, `Task ${tasks[id].status}: ${tasks[id].title}`)
|
|
127
127
|
}
|
|
128
128
|
if (tasks[id].agentId) {
|
|
129
|
-
requestHeartbeatNow({
|
|
129
|
+
requestHeartbeatNow({
|
|
130
|
+
agentId: tasks[id].agentId,
|
|
131
|
+
eventId: `task:${id}:${tasks[id].status}`,
|
|
132
|
+
reason: 'task-completed',
|
|
133
|
+
source: `task:${id}`,
|
|
134
|
+
resumeMessage: `Task ${tasks[id].status}: ${tasks[id].title}`,
|
|
135
|
+
detail: tasks[id].status === 'failed'
|
|
136
|
+
? String(tasks[id].error || '').slice(0, 400)
|
|
137
|
+
: JSON.stringify(tasks[id].result || '').slice(0, 400),
|
|
138
|
+
})
|
|
130
139
|
}
|
|
131
140
|
}
|
|
132
141
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import {
|
|
4
|
+
buildGitHubIssueTaskDescription,
|
|
5
|
+
buildGitHubIssueTaskTags,
|
|
6
|
+
buildGitHubIssueTaskTitle,
|
|
7
|
+
parseGitHubRepoInput,
|
|
8
|
+
} from './route'
|
|
9
|
+
|
|
10
|
+
test('parseGitHubRepoInput accepts repo slugs and GitHub URLs', () => {
|
|
11
|
+
assert.deepEqual(parseGitHubRepoInput('swarmclawai/swarmclaw'), {
|
|
12
|
+
owner: 'swarmclawai',
|
|
13
|
+
repo: 'swarmclaw',
|
|
14
|
+
fullName: 'swarmclawai/swarmclaw',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
assert.deepEqual(parseGitHubRepoInput('https://github.com/swarmclawai/swarmclaw/issues'), {
|
|
18
|
+
owner: 'swarmclawai',
|
|
19
|
+
repo: 'swarmclaw',
|
|
20
|
+
fullName: 'swarmclawai/swarmclaw',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
assert.equal(parseGitHubRepoInput('not-a-repo'), null)
|
|
24
|
+
assert.equal(parseGitHubRepoInput('https://example.com/swarmclawai/swarmclaw'), null)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('GitHub issue mapping builds a source-aware task payload shape', () => {
|
|
28
|
+
const issue = {
|
|
29
|
+
id: 12345,
|
|
30
|
+
number: 87,
|
|
31
|
+
title: 'Import GitHub issues into the board',
|
|
32
|
+
body: 'Bring open issues into SwarmClaw tasks.',
|
|
33
|
+
state: 'open',
|
|
34
|
+
html_url: 'https://github.com/swarmclawai/swarmclaw/issues/87',
|
|
35
|
+
labels: [{ name: 'feature' }, { name: 'task board' }, { name: 'feature' }],
|
|
36
|
+
assignee: { login: 'waydelyle' },
|
|
37
|
+
user: { login: 'octocat' },
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
assert.equal(
|
|
41
|
+
buildGitHubIssueTaskTitle(issue, 'swarmclawai/swarmclaw'),
|
|
42
|
+
'[swarmclawai/swarmclaw#87] Import GitHub issues into the board',
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
assert.equal(
|
|
46
|
+
buildGitHubIssueTaskDescription(issue, 'swarmclawai/swarmclaw'),
|
|
47
|
+
[
|
|
48
|
+
'Imported from GitHub issue swarmclawai/swarmclaw#87',
|
|
49
|
+
'URL: https://github.com/swarmclawai/swarmclaw/issues/87',
|
|
50
|
+
'State: open',
|
|
51
|
+
'Labels: feature, task board, feature',
|
|
52
|
+
'Assignee: waydelyle',
|
|
53
|
+
'Opened by: octocat',
|
|
54
|
+
'',
|
|
55
|
+
'Bring open issues into SwarmClaw tasks.',
|
|
56
|
+
].join('\n'),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
assert.deepEqual(buildGitHubIssueTaskTags(issue, 'swarmclawai/swarmclaw'), [
|
|
60
|
+
'github',
|
|
61
|
+
'swarmclawai/swarmclaw',
|
|
62
|
+
'feature',
|
|
63
|
+
'task board',
|
|
64
|
+
])
|
|
65
|
+
})
|
|
@@ -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')
|