@swarmclawai/swarmclaw 1.2.6 → 1.2.9
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 +54 -23
- package/next.config.ts +1 -0
- package/package.json +4 -3
- package/scripts/easy-setup.mjs +1 -1
- package/scripts/postinstall.mjs +1 -1
- package/skills/swarmclaw.md +115 -0
- package/skills/tools/browser.md +131 -0
- package/skills/tools/execute.md +98 -0
- package/skills/tools/files.md +98 -0
- package/skills/tools/memory.md +104 -0
- package/skills/tools/platform.md +144 -0
- package/skills/tools/skills.md +83 -0
- package/src/app/agents/[id]/page.tsx +1 -18
- package/src/app/api/agents/thread-route.test.ts +0 -1
- package/src/app/api/approvals/route.test.ts +6 -22
- package/src/app/api/chats/[id]/messages/route.ts +23 -19
- package/src/app/api/chats/messages-route.test.ts +105 -51
- package/src/app/api/connectors/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/test/route.ts +3 -2
- package/src/app/api/openclaw/deploy/route.ts +2 -0
- package/src/app/api/portability/export/route.ts +8 -0
- package/src/app/api/portability/import/route.test.ts +80 -0
- package/src/app/api/portability/import/route.ts +28 -0
- package/src/app/api/settings/route.ts +0 -2
- package/src/app/api/setup/doctor/route.ts +4 -4
- package/src/app/api/wallets/[id]/route.ts +15 -157
- package/src/app/api/wallets/generate/route.ts +22 -0
- package/src/app/api/wallets/route.test.ts +147 -0
- package/src/app/api/wallets/route.ts +13 -95
- package/src/app/autonomy/page.tsx +2 -57
- package/src/app/protocols/page.tsx +2 -21
- package/src/app/settings/page.tsx +0 -9
- package/src/app/wallets/page.tsx +105 -5
- package/src/cli/index.js +21 -33
- package/src/cli/spec.js +19 -30
- package/src/components/agents/agent-chat-list.tsx +23 -1
- package/src/components/agents/agent-sheet.tsx +2 -40
- package/src/components/agents/inspector-panel.tsx +165 -131
- package/src/components/chat/chat-area.tsx +38 -9
- package/src/components/chat/chat-card.tsx +0 -31
- package/src/components/chat/message-bubble.tsx +1 -108
- package/src/components/chat/message-list.tsx +33 -19
- package/src/components/connectors/connector-sheet.tsx +25 -1
- package/src/components/gateways/gateway-sheet.tsx +5 -2
- package/src/components/layout/sidebar-rail.tsx +6 -10
- package/src/components/projects/project-detail.tsx +3 -35
- package/src/components/projects/tabs/overview-tab.tsx +3 -59
- package/src/components/projects/tabs/work-tab.tsx +7 -77
- package/src/components/protocols/structured-session-launcher.tsx +1 -22
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/tasks/task-card.tsx +4 -34
- package/src/components/tasks/task-sheet.tsx +6 -36
- package/src/components/wallets/wallet-list.tsx +150 -0
- package/src/lib/agent-execute-defaults.test.ts +24 -0
- package/src/lib/agent-execute-defaults.ts +62 -0
- package/src/lib/app/navigation.test.ts +0 -13
- package/src/lib/app/navigation.ts +2 -7
- package/src/lib/app/view-constants.ts +14 -19
- package/src/lib/chat/queued-message-queue.test.ts +134 -1
- package/src/lib/chat/queued-message-queue.ts +77 -2
- package/src/lib/server/agents/agent-service.ts +5 -0
- package/src/lib/server/agents/agent-thread-session.ts +0 -1
- package/src/lib/server/agents/delegation-advisory.test.ts +0 -1
- package/src/lib/server/agents/delegation-jobs.test.ts +0 -69
- package/src/lib/server/agents/delegation-jobs.ts +0 -25
- package/src/lib/server/agents/main-agent-loop.ts +1 -49
- package/src/lib/server/agents/subagent-runtime.ts +0 -1
- package/src/lib/server/approval-match.ts +0 -85
- package/src/lib/server/approvals.test.ts +6 -6
- package/src/lib/server/approvals.ts +0 -6
- package/src/lib/server/autonomy/supervisor-reflection.test.ts +0 -1
- package/src/lib/server/builtin-extensions.ts +1 -2
- package/src/lib/server/capability-router.test.ts +0 -2
- package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +1 -1
- package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +15 -14
- package/src/lib/server/chat-execution/chat-execution-types.ts +0 -2
- package/src/lib/server/chat-execution/chat-execution-utils.ts +2 -4
- package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -30
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +1 -36
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +81 -64
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -0
- package/src/lib/server/chat-execution/continuation-evaluator.ts +8 -0
- package/src/lib/server/chat-execution/iteration-event-handler.ts +0 -24
- package/src/lib/server/chat-execution/memory-mutation-tools.ts +1 -1
- package/src/lib/server/chat-execution/message-classifier.test.ts +0 -45
- package/src/lib/server/chat-execution/message-classifier.ts +11 -16
- package/src/lib/server/chat-execution/prompt-builder.test.ts +27 -0
- package/src/lib/server/chat-execution/prompt-builder.ts +14 -31
- package/src/lib/server/chat-execution/prompt-mode.test.ts +24 -0
- package/src/lib/server/chat-execution/prompt-mode.ts +5 -1
- package/src/lib/server/chat-execution/prompt-sections.ts +0 -1
- package/src/lib/server/chat-execution/situational-awareness.test.ts +2 -73
- package/src/lib/server/chat-execution/situational-awareness.ts +4 -38
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +13 -126
- package/src/lib/server/chat-execution/stream-agent-chat.ts +46 -21
- package/src/lib/server/chat-execution/stream-continuation.test.ts +4 -52
- package/src/lib/server/chat-execution/stream-continuation.ts +6 -48
- package/src/lib/server/chatrooms/chatroom-routing.test.ts +4 -0
- package/src/lib/server/chatrooms/session-mailbox.ts +0 -10
- package/src/lib/server/chats/chat-session-service.ts +3 -5
- package/src/lib/server/connectors/connector-inbound.ts +0 -1
- package/src/lib/server/connectors/connector-lifecycle.ts +19 -3
- package/src/lib/server/connectors/connector-service.ts +39 -9
- package/src/lib/server/connectors/discord.ts +2 -2
- package/src/lib/server/connectors/matrix.ts +3 -2
- package/src/lib/server/connectors/signal.ts +5 -4
- package/src/lib/server/connectors/slack.ts +10 -9
- package/src/lib/server/connectors/swarmdock-bidding.ts +74 -0
- package/src/lib/server/connectors/swarmdock-payloads.test.ts +85 -0
- package/src/lib/server/connectors/swarmdock-secret.test.ts +128 -0
- package/src/lib/server/connectors/swarmdock-secret.ts +152 -0
- package/src/lib/server/connectors/swarmdock-tasks.ts +119 -0
- package/src/lib/server/connectors/swarmdock.ts +255 -0
- package/src/lib/server/connectors/teams.ts +3 -2
- package/src/lib/server/connectors/telegram.ts +4 -4
- package/src/lib/server/connectors/whatsapp.ts +2 -2
- package/src/lib/server/daemon/controller.ts +7 -0
- package/src/lib/server/execution-brief.test.ts +2 -25
- package/src/lib/server/execution-brief.ts +12 -35
- package/src/lib/server/execution-engine/task-attempt.ts +0 -1
- package/src/lib/server/gateways/gateway-profile-service.ts +19 -1
- package/src/lib/server/messages/message-repository.test.ts +70 -0
- package/src/lib/server/messages/message-repository.ts +11 -6
- package/src/lib/server/openclaw/deploy.ts +32 -2
- package/src/lib/server/persistence/storage-context.ts +0 -5
- package/src/lib/server/plugins-advanced.test.ts +1 -2
- package/src/lib/server/portability/export.ts +109 -0
- package/src/lib/server/portability/import.ts +159 -0
- package/src/lib/server/protocols/protocol-normalization.ts +0 -4
- package/src/lib/server/protocols/protocol-queries.ts +0 -6
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +4 -32
- package/src/lib/server/protocols/protocol-service.ts +0 -1
- package/src/lib/server/protocols/protocol-step-helpers.ts +0 -4
- package/src/lib/server/protocols/protocol-step-processors.ts +0 -6
- package/src/lib/server/protocols/protocol-swarm.ts +0 -2
- package/src/lib/server/protocols/protocol-types.ts +0 -2
- package/src/lib/server/provider-health.ts +1 -10
- package/src/lib/server/runtime/daemon-state/core.ts +0 -9
- package/src/lib/server/runtime/daemon-state.test.ts +0 -35
- package/src/lib/server/runtime/heartbeat-service.ts +3 -23
- package/src/lib/server/runtime/process-manager.ts +13 -9
- package/src/lib/server/runtime/queue/core.ts +11 -33
- package/src/lib/server/runtime/runtime-storage-write-paths.test.ts +6 -6
- package/src/lib/server/runtime/scheduler.ts +0 -13
- package/src/lib/server/runtime/session-run-manager/drain.ts +0 -24
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +0 -1
- package/src/lib/server/runtime/session-run-manager/queries.ts +15 -1
- package/src/lib/server/runtime/session-run-manager/recovery.ts +0 -1
- package/src/lib/server/runtime/session-run-manager.test.ts +58 -28
- package/src/lib/server/sandbox/session-runtime.test.ts +18 -1
- package/src/lib/server/sandbox/session-runtime.ts +40 -28
- package/src/lib/server/session-tools/autonomy-tools.test.ts +7 -9
- package/src/lib/server/session-tools/context.ts +1 -1
- package/src/lib/server/session-tools/credential-env.ts +109 -0
- package/src/lib/server/session-tools/crud.ts +3 -17
- package/src/lib/server/session-tools/delegate.ts +0 -4
- package/src/lib/server/session-tools/edit_file.ts +3 -2
- package/src/lib/server/session-tools/execute.test.ts +58 -0
- package/src/lib/server/session-tools/execute.ts +334 -0
- package/src/lib/server/session-tools/files-tool.ts +635 -0
- package/src/lib/server/session-tools/index.ts +14 -8
- package/src/lib/server/session-tools/memory-tool.ts +242 -0
- package/src/lib/server/session-tools/memory.ts +1 -1
- package/src/lib/server/session-tools/openclaw-nodes.ts +3 -2
- package/src/lib/server/session-tools/openclaw-workspace.ts +3 -2
- package/src/lib/server/session-tools/platform-tool.ts +617 -0
- package/src/lib/server/session-tools/session-info.ts +3 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +3 -4
- package/src/lib/server/session-tools/shell.ts +7 -122
- package/src/lib/server/session-tools/skills-tool.ts +396 -0
- package/src/lib/server/session-tools/team-context.ts +0 -3
- package/src/lib/server/session-tools/web.ts +2 -2
- package/src/lib/server/storage-normalization.ts +10 -0
- package/src/lib/server/storage.ts +18 -45
- package/src/lib/server/tasks/task-checkout.ts +59 -0
- package/src/lib/server/tasks/task-lifecycle.ts +2 -0
- package/src/lib/server/tasks/task-route-service.ts +4 -26
- package/src/lib/server/tasks/task-service.ts +0 -7
- package/src/lib/server/tool-aliases.ts +2 -2
- package/src/lib/server/tool-capability-policy-advanced.test.ts +13 -6
- package/src/lib/server/tool-capability-policy.test.ts +2 -1
- package/src/lib/server/tool-capability-policy.ts +60 -35
- package/src/lib/server/tool-planning.ts +11 -12
- package/src/lib/server/universal-tool-access.ts +0 -1
- package/src/lib/server/wallets/wallet-crypto.ts +33 -0
- package/src/lib/server/wallets/wallet-repository.ts +24 -0
- package/src/lib/server/wallets/wallet-service.ts +119 -0
- package/src/lib/server/working-state/extraction.ts +8 -42
- package/src/lib/server/working-state/normalization.ts +10 -103
- package/src/lib/server/working-state/service.ts +12 -21
- package/src/lib/setup-defaults.ts +5 -0
- package/src/lib/strip-internal-metadata.test.ts +1 -1
- package/src/lib/strip-internal-metadata.ts +1 -1
- package/src/lib/tool-definitions.ts +1 -1
- package/src/lib/validation/schemas.test.ts +16 -0
- package/src/lib/validation/schemas.ts +49 -2
- package/src/stores/slices/data-slice.ts +5 -1
- package/src/stores/slices/ui-slice.ts +0 -4
- package/src/stores/use-chat-store.test.ts +231 -0
- package/src/stores/use-chat-store.ts +62 -13
- package/src/types/agent.ts +264 -0
- package/src/types/app-settings.ts +173 -0
- package/src/types/approval.ts +25 -0
- package/src/types/connector.ts +188 -0
- package/src/types/extension.ts +386 -0
- package/src/types/index.ts +16 -3555
- package/src/types/message.ts +56 -0
- package/src/types/misc.ts +737 -0
- package/src/types/protocol.ts +420 -0
- package/src/types/provider.ts +52 -0
- package/src/types/run.ts +180 -0
- package/src/types/schedule.ts +59 -0
- package/src/types/session.ts +215 -0
- package/src/types/skill.ts +157 -0
- package/src/types/swarmdock.ts +29 -0
- package/src/types/task.ts +144 -0
- package/src/types/working-state.ts +204 -0
- package/src/views/settings/section-heartbeat.tsx +2 -2
- package/src/views/settings/section-runtime-loop.tsx +0 -14
- package/src/app/api/canvas/[sessionId]/route.ts +0 -35
- package/src/app/api/missions/[id]/actions/route.ts +0 -31
- package/src/app/api/missions/[id]/events/route.ts +0 -14
- package/src/app/api/missions/[id]/route.ts +0 -10
- package/src/app/api/missions/route.test.ts +0 -244
- package/src/app/api/missions/route.ts +0 -57
- package/src/app/api/wallets/[id]/approve/route.ts +0 -79
- package/src/app/api/wallets/[id]/balance-history/route.ts +0 -18
- package/src/app/api/wallets/[id]/send/route.ts +0 -113
- package/src/app/api/wallets/[id]/transactions/route.ts +0 -18
- package/src/app/missions/[id]/page.tsx +0 -3
- package/src/app/missions/page.tsx +0 -685
- package/src/components/canvas/canvas-panel.tsx +0 -267
- package/src/components/wallets/wallet-approval-dialog.tsx +0 -107
- package/src/components/wallets/wallet-panel.tsx +0 -1010
- package/src/components/wallets/wallet-section.tsx +0 -260
- package/src/features/missions/queries.ts +0 -23
- package/src/lib/canvas-content.test.ts +0 -360
- package/src/lib/canvas-content.ts +0 -198
- package/src/lib/server/canvas-content.test.ts +0 -32
- package/src/lib/server/canvas-content.ts +0 -6
- package/src/lib/server/ethereum.ts +0 -591
- package/src/lib/server/evm-swap.ts +0 -476
- package/src/lib/server/missions/mission-intent.test.ts +0 -63
- package/src/lib/server/missions/mission-intent.ts +0 -569
- package/src/lib/server/missions/mission-repository.ts +0 -74
- package/src/lib/server/missions/mission-service/actions.ts +0 -6
- package/src/lib/server/missions/mission-service/bindings.ts +0 -9
- package/src/lib/server/missions/mission-service/context.ts +0 -4
- package/src/lib/server/missions/mission-service/core.ts +0 -2271
- package/src/lib/server/missions/mission-service/queries.ts +0 -12
- package/src/lib/server/missions/mission-service/recovery.ts +0 -5
- package/src/lib/server/missions/mission-service/ticks.ts +0 -9
- package/src/lib/server/missions/mission-service.test.ts +0 -888
- package/src/lib/server/missions/mission-service.ts +0 -6
- package/src/lib/server/session-tools/canvas.ts +0 -105
- package/src/lib/server/session-tools/sandbox.ts +0 -281
- package/src/lib/server/session-tools/wallet-tool.test.ts +0 -150
- package/src/lib/server/session-tools/wallet.ts +0 -1287
- package/src/lib/server/solana.ts +0 -327
- package/src/lib/server/wallet/wallet-execution.test.ts +0 -198
- package/src/lib/server/wallet/wallet-portfolio.test.ts +0 -98
- package/src/lib/server/wallet/wallet-portfolio.ts +0 -772
- package/src/lib/server/wallet/wallet-service.test.ts +0 -81
- package/src/lib/server/wallet/wallet-service.ts +0 -225
- package/src/lib/wallet/wallet-transactions.test.ts +0 -75
- package/src/lib/wallet/wallet-transactions.ts +0 -43
- package/src/lib/wallet/wallet.test.ts +0 -333
- package/src/lib/wallet/wallet.ts +0 -183
- package/src/views/settings/section-wallets.tsx +0 -35
|
@@ -145,11 +145,12 @@ const slack: PlatformConnector = {
|
|
|
145
145
|
}
|
|
146
146
|
botUserId = auth.user_id as string
|
|
147
147
|
log.info(TAG, `Authenticated as @${auth.user} in workspace "${auth.team}"`)
|
|
148
|
-
} catch (err:
|
|
149
|
-
const hint = err.code
|
|
148
|
+
} catch (err: unknown) {
|
|
149
|
+
const hint = (err instanceof Error && 'code' in err) ? (err as { code: string }).code : undefined
|
|
150
|
+
const suffix = hint === 'slack_webapi_platform_error'
|
|
150
151
|
? '. Check that your Bot Token (xoxb-...) is correct and the app is installed to the workspace.'
|
|
151
152
|
: ''
|
|
152
|
-
throw new Error(`Slack auth failed: ${err
|
|
153
|
+
throw new Error(`Slack auth failed: ${errorMessage(err)}${suffix}`)
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
const app = new App({
|
|
@@ -215,8 +216,8 @@ const slack: PlatformConnector = {
|
|
|
215
216
|
media.push(stored)
|
|
216
217
|
continue
|
|
217
218
|
}
|
|
218
|
-
} catch (err:
|
|
219
|
-
log.warn(TAG, `Media download failed (${f?.name || 'file'}):`,
|
|
219
|
+
} catch (err: unknown) {
|
|
220
|
+
log.warn(TAG, `Media download failed (${f?.name || 'file'}):`, errorMessage(err))
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
media.push({
|
|
@@ -264,8 +265,8 @@ const slack: PlatformConnector = {
|
|
|
264
265
|
return sent.ts || undefined
|
|
265
266
|
},
|
|
266
267
|
})
|
|
267
|
-
} catch (err:
|
|
268
|
-
log.error(TAG, 'Error handling message:', err
|
|
268
|
+
} catch (err: unknown) {
|
|
269
|
+
log.error(TAG, 'Error handling message:', errorMessage(err))
|
|
269
270
|
try {
|
|
270
271
|
await say('Sorry, I encountered an error processing your message.')
|
|
271
272
|
} catch { /* ignore */ }
|
|
@@ -322,8 +323,8 @@ const slack: PlatformConnector = {
|
|
|
322
323
|
return sent.ts || undefined
|
|
323
324
|
},
|
|
324
325
|
})
|
|
325
|
-
} catch (err:
|
|
326
|
-
log.error(TAG, 'Error handling mention:', err
|
|
326
|
+
} catch (err: unknown) {
|
|
327
|
+
log.error(TAG, 'Error handling mention:', errorMessage(err))
|
|
327
328
|
}
|
|
328
329
|
})
|
|
329
330
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { log } from '@/lib/server/logger'
|
|
2
|
+
import type { BidCreateInput } from '@swarmdock/shared'
|
|
3
|
+
|
|
4
|
+
const TAG = 'swarmdock-bid'
|
|
5
|
+
|
|
6
|
+
interface SwarmDockTask {
|
|
7
|
+
id: string
|
|
8
|
+
title: string
|
|
9
|
+
skillRequirements: string[]
|
|
10
|
+
budgetMax: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SwarmDockConfig {
|
|
14
|
+
skills: string
|
|
15
|
+
maxBudget: string
|
|
16
|
+
autoDiscover: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Determine if the agent should auto-bid on a discovered task.
|
|
21
|
+
* Checks skill overlap and budget limits.
|
|
22
|
+
*/
|
|
23
|
+
export function shouldAutoBid(task: SwarmDockTask, config: SwarmDockConfig): boolean {
|
|
24
|
+
if (!config.autoDiscover) return false
|
|
25
|
+
|
|
26
|
+
// Check budget
|
|
27
|
+
const maxBudget = BigInt(config.maxBudget || '0')
|
|
28
|
+
if (maxBudget > BigInt(0)) {
|
|
29
|
+
const taskBudget = BigInt(task.budgetMax || '0')
|
|
30
|
+
if (taskBudget > maxBudget) {
|
|
31
|
+
log.debug(TAG, `Skipping "${task.title}" — budget ${task.budgetMax} exceeds max ${config.maxBudget}`)
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check skill overlap
|
|
37
|
+
const agentSkills = new Set(
|
|
38
|
+
config.skills.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean),
|
|
39
|
+
)
|
|
40
|
+
if (agentSkills.size === 0) return false
|
|
41
|
+
|
|
42
|
+
const hasMatchingSkill = task.skillRequirements.some(
|
|
43
|
+
(req) => agentSkills.has(req.toLowerCase()),
|
|
44
|
+
)
|
|
45
|
+
if (!hasMatchingSkill) {
|
|
46
|
+
log.debug(TAG, `Skipping "${task.title}" — no matching skills`)
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Submit an auto-bid on a SwarmDock task.
|
|
55
|
+
* Uses the task's max budget as the proposed price (simple strategy).
|
|
56
|
+
*/
|
|
57
|
+
export async function submitAutoBid(
|
|
58
|
+
client: { tasks: { bid: (taskId: string, input: BidCreateInput) => Promise<unknown> } },
|
|
59
|
+
taskId: string,
|
|
60
|
+
config: SwarmDockConfig,
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
const agentSkills = config.skills.split(',').map((s) => s.trim()).filter(Boolean)
|
|
63
|
+
|
|
64
|
+
const bid: BidCreateInput = {
|
|
65
|
+
proposedPrice: config.maxBudget || '1000000',
|
|
66
|
+
confidenceScore: 0.8,
|
|
67
|
+
proposal: `SwarmClaw agent with skills: ${agentSkills.join(', ')}. Ready to start immediately.`,
|
|
68
|
+
portfolioRefs: [],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await client.tasks.bid(taskId, bid)
|
|
72
|
+
|
|
73
|
+
log.info(TAG, `Auto-bid submitted for task ${taskId}`)
|
|
74
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { submitAutoBid } from '@/lib/server/connectors/swarmdock-bidding'
|
|
5
|
+
import { submitSwarmdockTaskResult } from '@/lib/server/connectors/swarmdock'
|
|
6
|
+
|
|
7
|
+
test('submitAutoBid includes empty portfolio refs for SDK compatibility', async () => {
|
|
8
|
+
const seen: {
|
|
9
|
+
taskId?: string
|
|
10
|
+
bid?: { proposedPrice: string; portfolioRefs: string[] }
|
|
11
|
+
} = {}
|
|
12
|
+
|
|
13
|
+
await submitAutoBid(
|
|
14
|
+
{
|
|
15
|
+
tasks: {
|
|
16
|
+
bid: async (taskId, input) => {
|
|
17
|
+
seen.taskId = taskId
|
|
18
|
+
seen.bid = {
|
|
19
|
+
proposedPrice: input.proposedPrice,
|
|
20
|
+
portfolioRefs: [...input.portfolioRefs],
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
'task-123',
|
|
26
|
+
{
|
|
27
|
+
skills: 'typescript,automation',
|
|
28
|
+
maxBudget: '2500000',
|
|
29
|
+
autoDiscover: true,
|
|
30
|
+
},
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assert.equal(seen.taskId, 'task-123')
|
|
34
|
+
assert.deepEqual(seen.bid, {
|
|
35
|
+
proposedPrice: '2500000',
|
|
36
|
+
portfolioRefs: [],
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('submitSwarmdockTaskResult includes empty files and propagates submit errors', async () => {
|
|
41
|
+
const seen: {
|
|
42
|
+
taskId?: string
|
|
43
|
+
payload?: { files: unknown[]; artifacts: Array<{ type: string; content: string }> }
|
|
44
|
+
} = {}
|
|
45
|
+
|
|
46
|
+
await submitSwarmdockTaskResult(
|
|
47
|
+
{
|
|
48
|
+
tasks: {
|
|
49
|
+
submit: async (taskId, input) => {
|
|
50
|
+
seen.taskId = taskId
|
|
51
|
+
seen.payload = {
|
|
52
|
+
files: [...input.files],
|
|
53
|
+
artifacts: input.artifacts.map((artifact) => ({
|
|
54
|
+
type: artifact.type,
|
|
55
|
+
content: String(artifact.content),
|
|
56
|
+
})),
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
'task-456',
|
|
62
|
+
'Result body',
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
assert.equal(seen.taskId, 'task-456')
|
|
66
|
+
assert.deepEqual(seen.payload, {
|
|
67
|
+
files: [],
|
|
68
|
+
artifacts: [{ type: 'text/markdown', content: 'Result body' }],
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
await assert.rejects(
|
|
72
|
+
submitSwarmdockTaskResult(
|
|
73
|
+
{
|
|
74
|
+
tasks: {
|
|
75
|
+
submit: async () => {
|
|
76
|
+
throw new Error('submit failed')
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
'task-456',
|
|
81
|
+
'Result body',
|
|
82
|
+
),
|
|
83
|
+
/submit failed/,
|
|
84
|
+
)
|
|
85
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import test from 'node:test'
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
|
|
9
|
+
|
|
10
|
+
function runWithTempDataDir(script: string) {
|
|
11
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-swarmdock-secret-'))
|
|
12
|
+
try {
|
|
13
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
+
cwd: repoRoot,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
CREDENTIAL_SECRET: 'test-credential-secret',
|
|
18
|
+
DATA_DIR: path.join(tempDir, 'data'),
|
|
19
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
20
|
+
},
|
|
21
|
+
encoding: 'utf-8',
|
|
22
|
+
})
|
|
23
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
24
|
+
const lines = (result.stdout || '')
|
|
25
|
+
.trim()
|
|
26
|
+
.split('\n')
|
|
27
|
+
.map((line) => line.trim())
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
30
|
+
return JSON.parse(jsonLine || '{}')
|
|
31
|
+
} finally {
|
|
32
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('createConnector stores SwarmDock private keys as credentials and redacts config output', () => {
|
|
37
|
+
const output = runWithTempDataDir(`
|
|
38
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
39
|
+
const serviceMod = await import('./src/lib/server/connectors/connector-service')
|
|
40
|
+
const secretMod = await import('./src/lib/server/connectors/swarmdock-secret')
|
|
41
|
+
const storage = storageMod.default || storageMod
|
|
42
|
+
const service = serviceMod.default || serviceMod
|
|
43
|
+
const secret = secretMod.default || secretMod
|
|
44
|
+
|
|
45
|
+
const created = service.createConnector({
|
|
46
|
+
name: 'SwarmDock Worker',
|
|
47
|
+
platform: 'swarmdock',
|
|
48
|
+
config: {
|
|
49
|
+
apiUrl: 'https://api.swarmdock.example',
|
|
50
|
+
walletAddress: '0x000000000000000000000000000000000000dEaD',
|
|
51
|
+
privateKey: 'legacy-private-key',
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const stored = storage.loadConnectors()[created.id]
|
|
56
|
+
const credentials = storage.loadCredentials()
|
|
57
|
+
const credential = stored?.credentialId ? credentials[stored.credentialId] : null
|
|
58
|
+
const redacted = secret.redactConnectorSecrets({
|
|
59
|
+
...stored,
|
|
60
|
+
config: {
|
|
61
|
+
...(stored?.config || {}),
|
|
62
|
+
privateKey: 'should-not-leak',
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
console.log(JSON.stringify({
|
|
67
|
+
credentialId: stored?.credentialId || null,
|
|
68
|
+
storedHasPrivateKey: Boolean(stored?.config && Object.prototype.hasOwnProperty.call(stored.config, 'privateKey')),
|
|
69
|
+
credentialProvider: credential?.provider || null,
|
|
70
|
+
credentialName: credential?.name || null,
|
|
71
|
+
redactedHasPrivateKey: Boolean(redacted?.config && Object.prototype.hasOwnProperty.call(redacted.config, 'privateKey')),
|
|
72
|
+
}))
|
|
73
|
+
`)
|
|
74
|
+
|
|
75
|
+
assert.match(String(output.credentialId || ''), /^cred_/)
|
|
76
|
+
assert.equal(output.storedHasPrivateKey, false)
|
|
77
|
+
assert.equal(output.credentialProvider, 'swarmdock')
|
|
78
|
+
assert.match(String(output.credentialName || ''), /SwarmDock Identity Key/)
|
|
79
|
+
assert.equal(output.redactedHasPrivateKey, false)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('ensureSwarmdockConnectorCredential migrates stored legacy config keys into credentials', () => {
|
|
83
|
+
const output = runWithTempDataDir(`
|
|
84
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
85
|
+
const repoMod = await import('./src/lib/server/connectors/connector-repository')
|
|
86
|
+
const secretMod = await import('./src/lib/server/connectors/swarmdock-secret')
|
|
87
|
+
const storage = storageMod.default || storageMod
|
|
88
|
+
const repo = repoMod.default || repoMod
|
|
89
|
+
const secret = secretMod.default || secretMod
|
|
90
|
+
|
|
91
|
+
storage.saveConnectors({
|
|
92
|
+
conn_legacy: {
|
|
93
|
+
id: 'conn_legacy',
|
|
94
|
+
name: 'Legacy SwarmDock',
|
|
95
|
+
platform: 'swarmdock',
|
|
96
|
+
agentId: null,
|
|
97
|
+
chatroomId: null,
|
|
98
|
+
credentialId: null,
|
|
99
|
+
config: {
|
|
100
|
+
walletAddress: '0x000000000000000000000000000000000000dEaD',
|
|
101
|
+
privateKey: 'legacy-private-key',
|
|
102
|
+
},
|
|
103
|
+
isEnabled: false,
|
|
104
|
+
status: 'stopped',
|
|
105
|
+
lastError: null,
|
|
106
|
+
createdAt: 1,
|
|
107
|
+
updatedAt: 1,
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const prepared = secret.ensureSwarmdockConnectorCredential(repo.loadConnector('conn_legacy'))
|
|
112
|
+
const migrated = repo.loadConnector('conn_legacy')
|
|
113
|
+
const credentials = storage.loadCredentials()
|
|
114
|
+
const credential = migrated?.credentialId ? credentials[migrated.credentialId] : null
|
|
115
|
+
|
|
116
|
+
console.log(JSON.stringify({
|
|
117
|
+
fallbackPrivateKey: prepared.fallbackPrivateKey,
|
|
118
|
+
migratedCredentialId: migrated?.credentialId || null,
|
|
119
|
+
migratedHasPrivateKey: Boolean(migrated?.config && Object.prototype.hasOwnProperty.call(migrated.config, 'privateKey')),
|
|
120
|
+
credentialProvider: credential?.provider || null,
|
|
121
|
+
}))
|
|
122
|
+
`)
|
|
123
|
+
|
|
124
|
+
assert.equal(output.fallbackPrivateKey, null)
|
|
125
|
+
assert.match(String(output.migratedCredentialId || ''), /^cred_/)
|
|
126
|
+
assert.equal(output.migratedHasPrivateKey, false)
|
|
127
|
+
assert.equal(output.credentialProvider, 'swarmdock')
|
|
128
|
+
})
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { Connector } from '@/types'
|
|
2
|
+
import { createCredentialRecord, resolveCredentialSecret } from '@/lib/server/credentials/credential-service'
|
|
3
|
+
import { upsertConnector } from './connector-repository'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
|
|
6
|
+
export const SWARMMDOCK_CREDENTIAL_PROVIDER = 'swarmdock'
|
|
7
|
+
|
|
8
|
+
function clean(value: unknown): string {
|
|
9
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function cloneConfig(config: Record<string, string> | null | undefined): Record<string, string> {
|
|
13
|
+
return config ? { ...config } : {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getLegacyPrivateKey(config: Record<string, string> | null | undefined): string {
|
|
17
|
+
return clean(config?.privateKey)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stripLegacyPrivateKey(config: Record<string, string> | null | undefined): Record<string, string> {
|
|
21
|
+
const next = cloneConfig(config)
|
|
22
|
+
delete next.privateKey
|
|
23
|
+
return next
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildCredentialName(connectorName: string): string {
|
|
27
|
+
const normalizedName = clean(connectorName)
|
|
28
|
+
return normalizedName ? `${normalizedName} SwarmDock Identity Key` : 'SwarmDock Identity Key'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function persistConnectorSecretMigration(
|
|
32
|
+
connector: Connector,
|
|
33
|
+
credentialId: string,
|
|
34
|
+
): Connector {
|
|
35
|
+
const nextConfig = stripLegacyPrivateKey(connector.config)
|
|
36
|
+
const next: Connector = {
|
|
37
|
+
...connector,
|
|
38
|
+
credentialId,
|
|
39
|
+
config: nextConfig,
|
|
40
|
+
updatedAt: Date.now(),
|
|
41
|
+
}
|
|
42
|
+
upsertConnector(next.id, next)
|
|
43
|
+
notify('connectors')
|
|
44
|
+
return next
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function redactConnectorSecrets<T extends Connector>(connector: T): T {
|
|
48
|
+
if (connector.platform !== 'swarmdock') {
|
|
49
|
+
return {
|
|
50
|
+
...connector,
|
|
51
|
+
config: cloneConfig(connector.config),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
...connector,
|
|
56
|
+
config: stripLegacyPrivateKey(connector.config),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function prepareSwarmdockConnectorInput(params: {
|
|
61
|
+
platform: Connector['platform']
|
|
62
|
+
name: string
|
|
63
|
+
credentialId: string | null
|
|
64
|
+
config: Record<string, string> | null | undefined
|
|
65
|
+
}): {
|
|
66
|
+
credentialId: string | null
|
|
67
|
+
config: Record<string, string>
|
|
68
|
+
} {
|
|
69
|
+
const config = cloneConfig(params.config)
|
|
70
|
+
if (params.platform !== 'swarmdock') {
|
|
71
|
+
return {
|
|
72
|
+
credentialId: clean(params.credentialId) || null,
|
|
73
|
+
config,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const credentialId = clean(params.credentialId)
|
|
78
|
+
const legacyPrivateKey = getLegacyPrivateKey(config)
|
|
79
|
+
if (!legacyPrivateKey) {
|
|
80
|
+
return {
|
|
81
|
+
credentialId: credentialId || null,
|
|
82
|
+
config: stripLegacyPrivateKey(config),
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (credentialId) {
|
|
87
|
+
return {
|
|
88
|
+
credentialId,
|
|
89
|
+
config: stripLegacyPrivateKey(config),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const credential = createCredentialRecord({
|
|
94
|
+
provider: SWARMMDOCK_CREDENTIAL_PROVIDER,
|
|
95
|
+
name: buildCredentialName(params.name),
|
|
96
|
+
apiKey: legacyPrivateKey,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
credentialId: credential.id,
|
|
101
|
+
config: stripLegacyPrivateKey(config),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function ensureSwarmdockConnectorCredential(
|
|
106
|
+
connector: Connector,
|
|
107
|
+
options?: { allowMigrationFailureFallback?: boolean },
|
|
108
|
+
): {
|
|
109
|
+
connector: Connector
|
|
110
|
+
fallbackPrivateKey: string | null
|
|
111
|
+
} {
|
|
112
|
+
if (connector.platform !== 'swarmdock') {
|
|
113
|
+
return { connector, fallbackPrivateKey: null }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const legacyPrivateKey = getLegacyPrivateKey(connector.config)
|
|
117
|
+
if (!legacyPrivateKey) {
|
|
118
|
+
return { connector, fallbackPrivateKey: null }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const configuredCredentialId = clean(connector.credentialId)
|
|
122
|
+
if (configuredCredentialId) {
|
|
123
|
+
if (resolveCredentialSecret(configuredCredentialId)) {
|
|
124
|
+
return {
|
|
125
|
+
connector: persistConnectorSecretMigration(connector, configuredCredentialId),
|
|
126
|
+
fallbackPrivateKey: null,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
connector,
|
|
131
|
+
fallbackPrivateKey: legacyPrivateKey,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const credential = createCredentialRecord({
|
|
137
|
+
provider: SWARMMDOCK_CREDENTIAL_PROVIDER,
|
|
138
|
+
name: buildCredentialName(connector.name),
|
|
139
|
+
apiKey: legacyPrivateKey,
|
|
140
|
+
})
|
|
141
|
+
return {
|
|
142
|
+
connector: persistConnectorSecretMigration(connector, credential.id),
|
|
143
|
+
fallbackPrivateKey: null,
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (!options?.allowMigrationFailureFallback) throw error
|
|
147
|
+
return {
|
|
148
|
+
connector,
|
|
149
|
+
fallbackPrivateKey: legacyPrivateKey,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
|
+
import { loadTasks, saveTasks } from '@/lib/server/tasks/task-repository'
|
|
3
|
+
import { logActivity } from '@/lib/server/activity/activity-log'
|
|
4
|
+
import type { BoardTask } from '@/types/task'
|
|
5
|
+
|
|
6
|
+
interface SwarmDockTask {
|
|
7
|
+
id: string
|
|
8
|
+
requesterId: string
|
|
9
|
+
title: string
|
|
10
|
+
description: string
|
|
11
|
+
skillRequirements: string[]
|
|
12
|
+
budgetMax: string
|
|
13
|
+
deadline: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a SwarmClaw BoardTask from a SwarmDock task assignment.
|
|
18
|
+
* Uses `externalSource` to link back to the SwarmDock task (same pattern as GitHub issue import).
|
|
19
|
+
*/
|
|
20
|
+
export async function createBoardTaskFromAssignment(
|
|
21
|
+
task: SwarmDockTask,
|
|
22
|
+
agentId: string,
|
|
23
|
+
connectorId: string,
|
|
24
|
+
apiUrl: string,
|
|
25
|
+
): Promise<string> {
|
|
26
|
+
const tasks = loadTasks() as Record<string, BoardTask>
|
|
27
|
+
const id = genId()
|
|
28
|
+
const now = Date.now()
|
|
29
|
+
|
|
30
|
+
const boardTask: BoardTask = {
|
|
31
|
+
id,
|
|
32
|
+
title: task.title,
|
|
33
|
+
description: task.description,
|
|
34
|
+
status: 'running',
|
|
35
|
+
agentId,
|
|
36
|
+
createdAt: now,
|
|
37
|
+
updatedAt: now,
|
|
38
|
+
startedAt: now,
|
|
39
|
+
lastActivityAt: now,
|
|
40
|
+
sourceType: 'import',
|
|
41
|
+
externalSource: {
|
|
42
|
+
source: 'swarmdock',
|
|
43
|
+
id: task.id,
|
|
44
|
+
state: 'in_progress',
|
|
45
|
+
url: `${apiUrl}/tasks/${task.id}`,
|
|
46
|
+
},
|
|
47
|
+
tags: task.skillRequirements,
|
|
48
|
+
objective: task.description,
|
|
49
|
+
followupConnectorId: connectorId,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (task.deadline) {
|
|
53
|
+
boardTask.dueAt = new Date(task.deadline).getTime()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
tasks[id] = boardTask
|
|
57
|
+
saveTasks(tasks)
|
|
58
|
+
|
|
59
|
+
logActivity({
|
|
60
|
+
entityType: 'task',
|
|
61
|
+
entityId: id,
|
|
62
|
+
action: 'created',
|
|
63
|
+
actor: 'system',
|
|
64
|
+
summary: `SwarmDock task assigned: "${task.title}"`,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return id
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Update a SwarmClaw BoardTask based on a SwarmDock SSE event.
|
|
72
|
+
*/
|
|
73
|
+
export async function updateBoardTaskFromEvent(
|
|
74
|
+
swarmdockTaskId: string,
|
|
75
|
+
eventType: string,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
const tasks = loadTasks() as Record<string, BoardTask>
|
|
78
|
+
const boardTask = Object.values(tasks).find(
|
|
79
|
+
(t) => t.externalSource?.source === 'swarmdock' && t.externalSource.id === swarmdockTaskId,
|
|
80
|
+
)
|
|
81
|
+
if (!boardTask) return
|
|
82
|
+
|
|
83
|
+
const now = Date.now()
|
|
84
|
+
|
|
85
|
+
switch (eventType) {
|
|
86
|
+
case 'task.completed':
|
|
87
|
+
boardTask.status = 'completed'
|
|
88
|
+
boardTask.completedAt = now
|
|
89
|
+
boardTask.checkoutRunId = null
|
|
90
|
+
break
|
|
91
|
+
case 'task.submitted':
|
|
92
|
+
// Results submitted, waiting for approval on SwarmDock
|
|
93
|
+
if (boardTask.externalSource) boardTask.externalSource.state = 'review'
|
|
94
|
+
break
|
|
95
|
+
case 'task.cancelled':
|
|
96
|
+
boardTask.status = 'cancelled'
|
|
97
|
+
boardTask.checkoutRunId = null
|
|
98
|
+
break
|
|
99
|
+
case 'task.failed':
|
|
100
|
+
boardTask.status = 'failed'
|
|
101
|
+
boardTask.checkoutRunId = null
|
|
102
|
+
break
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
boardTask.updatedAt = now
|
|
106
|
+
boardTask.lastActivityAt = now
|
|
107
|
+
saveTasks(tasks)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Find a SwarmClaw BoardTask ID by its SwarmDock task ID.
|
|
112
|
+
*/
|
|
113
|
+
export function findBoardTaskBySwarmdockId(swarmdockTaskId: string): string | null {
|
|
114
|
+
const tasks = loadTasks() as Record<string, BoardTask>
|
|
115
|
+
const task = Object.values(tasks).find(
|
|
116
|
+
(t) => t.externalSource?.source === 'swarmdock' && t.externalSource.id === swarmdockTaskId,
|
|
117
|
+
)
|
|
118
|
+
return task?.id || null
|
|
119
|
+
}
|