@swarmclawai/swarmclaw 0.7.2 → 0.7.4
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 +116 -50
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +43 -0
- package/src/app/api/agents/[id]/thread/route.ts +39 -8
- package/src/app/api/agents/route.ts +35 -2
- package/src/app/api/auth/route.ts +77 -8
- package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +30 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +23 -1
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +12 -4
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +55 -17
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +16 -6
- package/src/app/api/tasks/bulk/route.ts +3 -3
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +135 -17
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +38 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +21 -12
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +456 -23
- package/src/components/agents/inspector-panel.tsx +110 -49
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +70 -27
- package/src/components/chat/chat-card.tsx +6 -21
- package/src/components/chat/chat-header.tsx +263 -366
- package/src/components/chat/chat-list.tsx +62 -26
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +145 -19
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +422 -209
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +385 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +189 -1
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +88 -6
- package/src/components/shared/settings/section-orchestrator.tsx +6 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +248 -47
- package/src/components/tasks/approvals-panel.tsx +211 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +264 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +44 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
- package/src/lib/server/chat-execution.ts +402 -125
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +74 -2
- package/src/lib/server/chatroom-helpers.ts +144 -11
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +994 -130
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +189 -10
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/daemon-state.ts +62 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +23 -43
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +31 -964
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +6 -5
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +18 -8
- package/src/lib/server/orchestrator.ts +5 -4
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +215 -0
- package/src/lib/server/plugins.ts +832 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +4 -21
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +96 -34
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +40 -12
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +243 -24
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +87 -2
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +162 -12
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +95 -25
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +58 -4
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +195 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +947 -108
- package/src/lib/server/storage.ts +255 -10
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +185 -25
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -11
- package/src/lib/server/tool-aliases.ts +80 -12
- package/src/lib/server/tool-capability-policy.ts +7 -1
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +62 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +43 -7
- package/src/stores/use-chat-store.ts +31 -2
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +470 -44
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -1,23 +1,112 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
3
|
import fs from 'fs'
|
|
4
|
-
import path from 'path'
|
|
5
4
|
import * as os from 'os'
|
|
6
5
|
import type { ToolBuildContext } from './context'
|
|
7
6
|
import { getPluginManager } from '../plugins'
|
|
8
7
|
import type { Plugin, PluginHooks } from '@/types'
|
|
9
8
|
import { safePath, truncate } from './context'
|
|
10
9
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
10
|
+
import { cancelWatchJob, createWatchJob, getWatchJob, listWatchJobs } from '../watch-jobs'
|
|
11
|
+
import { ensureSessionBrowserProfileId, loadBrowserSessionRecord } from '../browser-state'
|
|
12
|
+
|
|
13
|
+
type WatchKind = 'time' | 'http' | 'file' | 'task' | 'webhook' | 'page'
|
|
14
|
+
|
|
15
|
+
async function createDurableWatch(
|
|
16
|
+
normalized: Record<string, unknown>,
|
|
17
|
+
bctx: { cwd: string; sessionId?: string; agentId?: string | null },
|
|
18
|
+
explicitType?: WatchKind,
|
|
19
|
+
) {
|
|
20
|
+
const watchType = (explicitType || String(normalized.watchType || normalized.type || '').trim().toLowerCase()) as WatchKind
|
|
21
|
+
if (!watchType) return 'Error: watchType is required.'
|
|
22
|
+
if (!['time', 'http', 'file', 'task', 'webhook', 'page'].includes(watchType)) {
|
|
23
|
+
return `Error: Unsupported watchType "${watchType}".`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const sessionId = typeof normalized.sessionId === 'string' ? normalized.sessionId : bctx.sessionId
|
|
27
|
+
const agentId = typeof normalized.agentId === 'string' ? normalized.agentId : (bctx.agentId || undefined)
|
|
28
|
+
const resumeMessage = String(normalized.resumeMessage || normalized.message || '').trim()
|
|
29
|
+
if (!resumeMessage) return 'Error: resumeMessage is required.'
|
|
30
|
+
|
|
31
|
+
const target = (normalized.target ?? normalized.url ?? normalized.path) as string | undefined
|
|
32
|
+
const delayMinutes = typeof normalized.delayMinutes === 'number' ? normalized.delayMinutes : undefined
|
|
33
|
+
const runAt = typeof normalized.runAt === 'number'
|
|
34
|
+
? normalized.runAt
|
|
35
|
+
: delayMinutes !== undefined
|
|
36
|
+
? Date.now() + Math.max(0, delayMinutes) * 60_000
|
|
37
|
+
: undefined
|
|
38
|
+
const intervalMs = typeof normalized.intervalSec === 'number'
|
|
39
|
+
? Math.max(15, normalized.intervalSec) * 1000
|
|
40
|
+
: typeof normalized.intervalMs === 'number'
|
|
41
|
+
? Math.max(15_000, normalized.intervalMs)
|
|
42
|
+
: undefined
|
|
43
|
+
const timeoutAt = typeof normalized.timeoutMinutes === 'number'
|
|
44
|
+
? Date.now() + Math.max(1, normalized.timeoutMinutes) * 60_000
|
|
45
|
+
: typeof normalized.timeoutAt === 'number'
|
|
46
|
+
? normalized.timeoutAt
|
|
47
|
+
: undefined
|
|
48
|
+
const browserProfileId = sessionId ? ensureSessionBrowserProfileId(sessionId).profileId : null
|
|
49
|
+
const targetPath = watchType === 'file' && target ? safePath(bctx.cwd, target) : target
|
|
50
|
+
const pageUrl = watchType === 'page' && !target && sessionId
|
|
51
|
+
? loadBrowserSessionRecord(sessionId)?.currentUrl || undefined
|
|
52
|
+
: undefined
|
|
53
|
+
const pageTarget = target || pageUrl
|
|
54
|
+
if ((watchType === 'http' || watchType === 'page') && !pageTarget) {
|
|
55
|
+
return `Error: ${watchType === 'page' ? 'url or active browser page' : 'url'} is required.`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const job = await createWatchJob({
|
|
59
|
+
type: watchType,
|
|
60
|
+
sessionId: sessionId || null,
|
|
61
|
+
agentId: agentId || null,
|
|
62
|
+
createdByAgentId: agentId || null,
|
|
63
|
+
browserProfileId,
|
|
64
|
+
description: typeof normalized.description === 'string' ? normalized.description : null,
|
|
65
|
+
resumeMessage,
|
|
66
|
+
runAt,
|
|
67
|
+
intervalMs,
|
|
68
|
+
timeoutAt,
|
|
69
|
+
target: {
|
|
70
|
+
url: watchType === 'http' || watchType === 'page' ? pageTarget : undefined,
|
|
71
|
+
path: watchType === 'file' ? targetPath : undefined,
|
|
72
|
+
taskId: watchType === 'task' ? String(normalized.taskId || normalized.id || '') : undefined,
|
|
73
|
+
webhookId: watchType === 'webhook' ? String(normalized.webhookId || normalized.id || '') : undefined,
|
|
74
|
+
baselineHash: undefined,
|
|
75
|
+
},
|
|
76
|
+
condition: {
|
|
77
|
+
containsText: typeof normalized.containsText === 'string' ? normalized.containsText : undefined,
|
|
78
|
+
textGone: typeof normalized.textGone === 'string' ? normalized.textGone : undefined,
|
|
79
|
+
regex: typeof normalized.regex === 'string' ? normalized.regex : undefined,
|
|
80
|
+
changed: normalized.changed === true,
|
|
81
|
+
exists: normalized.exists,
|
|
82
|
+
status: typeof normalized.status === 'number' ? normalized.status : undefined,
|
|
83
|
+
statusIn: Array.isArray(normalized.statusIn) ? normalized.statusIn : undefined,
|
|
84
|
+
event: typeof normalized.event === 'string' ? normalized.event : undefined,
|
|
85
|
+
threshold: typeof normalized.threshold === 'number' ? normalized.threshold : undefined,
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
return JSON.stringify(job, null, 2)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getErrorMessage(err: unknown): string {
|
|
92
|
+
return err instanceof Error ? err.message : String(err)
|
|
93
|
+
}
|
|
11
94
|
|
|
12
95
|
/**
|
|
13
96
|
* Unified Monitoring Logic
|
|
14
97
|
*/
|
|
15
|
-
async function executeMonitorAction(
|
|
98
|
+
async function executeMonitorAction(
|
|
99
|
+
args: Record<string, unknown> | undefined,
|
|
100
|
+
bctx: { cwd: string; sessionId?: string; agentId?: string | null },
|
|
101
|
+
) {
|
|
16
102
|
const normalized = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
|
|
17
103
|
const action = normalized.action as string | undefined
|
|
18
104
|
const target = (normalized.target ?? normalized.url ?? normalized.path) as string | undefined
|
|
19
105
|
const limit = normalized.limit as number | undefined
|
|
20
106
|
const threshold = normalized.threshold as number | undefined
|
|
107
|
+
const sessionId = typeof normalized.sessionId === 'string' ? normalized.sessionId : bctx.sessionId
|
|
108
|
+
void limit
|
|
109
|
+
void sessionId
|
|
21
110
|
|
|
22
111
|
try {
|
|
23
112
|
switch (action) {
|
|
@@ -65,22 +154,72 @@ async function executeMonitorAction(args: any, bctx: { cwd: string }) {
|
|
|
65
154
|
status: res.status,
|
|
66
155
|
ok: res.ok,
|
|
67
156
|
latency: `${latency}ms`,
|
|
157
|
+
thresholdExceeded: typeof threshold === 'number' ? latency >= threshold : undefined,
|
|
68
158
|
url
|
|
69
159
|
}, null, 2)
|
|
70
|
-
} catch (err:
|
|
160
|
+
} catch (err: unknown) {
|
|
71
161
|
return JSON.stringify({
|
|
72
162
|
status: 'error',
|
|
73
|
-
error: err
|
|
163
|
+
error: getErrorMessage(err),
|
|
74
164
|
url
|
|
75
165
|
}, null, 2)
|
|
76
166
|
}
|
|
77
167
|
}
|
|
78
168
|
|
|
169
|
+
case 'create_watch': {
|
|
170
|
+
return createDurableWatch(normalized, bctx)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case 'wait_until': {
|
|
174
|
+
return createDurableWatch(normalized, bctx, 'time')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case 'wait_for_http': {
|
|
178
|
+
return createDurableWatch(normalized, bctx, 'http')
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case 'wait_for_file': {
|
|
182
|
+
return createDurableWatch(normalized, bctx, 'file')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
case 'wait_for_task': {
|
|
186
|
+
return createDurableWatch(normalized, bctx, 'task')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case 'wait_for_webhook': {
|
|
190
|
+
return createDurableWatch(normalized, bctx, 'webhook')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
case 'wait_for_page_change': {
|
|
194
|
+
return createDurableWatch(normalized, bctx, 'page')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case 'list_watches': {
|
|
198
|
+
const filterSessionId = normalized.all === true ? undefined : sessionId
|
|
199
|
+
return JSON.stringify(listWatchJobs({ sessionId: filterSessionId || null }), null, 2)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case 'get_watch': {
|
|
203
|
+
const id = String(normalized.id || '').trim()
|
|
204
|
+
if (!id) return 'Error: id is required.'
|
|
205
|
+
const job = getWatchJob(id)
|
|
206
|
+
if (!job) return `Error: watch job "${id}" not found.`
|
|
207
|
+
return JSON.stringify(job, null, 2)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case 'cancel_watch': {
|
|
211
|
+
const id = String(normalized.id || '').trim()
|
|
212
|
+
if (!id) return 'Error: id is required.'
|
|
213
|
+
const job = cancelWatchJob(id)
|
|
214
|
+
if (!job) return `Error: watch job "${id}" not found.`
|
|
215
|
+
return JSON.stringify(job, null, 2)
|
|
216
|
+
}
|
|
217
|
+
|
|
79
218
|
default:
|
|
80
219
|
return `Error: Unknown action "${action}"`
|
|
81
220
|
}
|
|
82
|
-
} catch (err:
|
|
83
|
-
return `Error: ${err
|
|
221
|
+
} catch (err: unknown) {
|
|
222
|
+
return `Error: ${getErrorMessage(err)}`
|
|
84
223
|
}
|
|
85
224
|
}
|
|
86
225
|
|
|
@@ -89,22 +228,29 @@ async function executeMonitorAction(args: any, bctx: { cwd: string }) {
|
|
|
89
228
|
*/
|
|
90
229
|
const MonitorPlugin: Plugin = {
|
|
91
230
|
name: 'Core Monitor',
|
|
92
|
-
description: 'System observability:
|
|
231
|
+
description: 'System observability and durable watch jobs: inspect system state, monitor files/endpoints/tasks, and resume agents when conditions trigger.',
|
|
93
232
|
hooks: {} as PluginHooks,
|
|
94
233
|
tools: [
|
|
95
234
|
{
|
|
96
235
|
name: 'monitor_tool',
|
|
97
|
-
description: 'Observe system health,
|
|
236
|
+
description: 'Observe system health, inspect logs/endpoints, or create durable waits like wait_for_http, wait_for_file, wait_for_webhook, and wait_for_page_change.',
|
|
98
237
|
parameters: {
|
|
99
238
|
type: 'object',
|
|
100
239
|
properties: {
|
|
101
|
-
action: { type: 'string', enum: ['sys_info', 'watch_log', 'ping'] },
|
|
240
|
+
action: { type: 'string', enum: ['sys_info', 'watch_log', 'ping', 'create_watch', 'wait_until', 'wait_for_http', 'wait_for_file', 'wait_for_task', 'wait_for_webhook', 'wait_for_page_change', 'list_watches', 'get_watch', 'cancel_watch'] },
|
|
102
241
|
target: { type: 'string', description: 'Log file path (for watch_log) or URL (for ping)' },
|
|
103
|
-
limit: { type: 'number', description: 'Number of lines or bytes to retrieve' }
|
|
242
|
+
limit: { type: 'number', description: 'Number of lines or bytes to retrieve' },
|
|
243
|
+
watchType: { type: 'string', enum: ['time', 'http', 'file', 'task', 'webhook', 'page'] },
|
|
244
|
+
resumeMessage: { type: 'string', description: 'Message injected when the watch triggers and the agent wakes up.' },
|
|
245
|
+
regex: { type: 'string', description: 'Regex pattern used by file/page/http watchers.' },
|
|
104
246
|
},
|
|
105
247
|
required: ['action']
|
|
106
248
|
},
|
|
107
|
-
execute: async (args, context) => executeMonitorAction(args, {
|
|
249
|
+
execute: async (args, context) => executeMonitorAction(args, {
|
|
250
|
+
cwd: context.session.cwd || process.cwd(),
|
|
251
|
+
sessionId: context.session.id,
|
|
252
|
+
agentId: context.session.agentId,
|
|
253
|
+
})
|
|
108
254
|
}
|
|
109
255
|
]
|
|
110
256
|
}
|
|
@@ -115,7 +261,11 @@ export function buildMonitorTools(bctx: ToolBuildContext): StructuredToolInterfa
|
|
|
115
261
|
if (!bctx.hasPlugin('monitor')) return []
|
|
116
262
|
return [
|
|
117
263
|
tool(
|
|
118
|
-
async (args) => executeMonitorAction(args, {
|
|
264
|
+
async (args) => executeMonitorAction(args, {
|
|
265
|
+
cwd: bctx.cwd,
|
|
266
|
+
sessionId: bctx.ctx?.sessionId || undefined,
|
|
267
|
+
agentId: bctx.ctx?.agentId || undefined,
|
|
268
|
+
}),
|
|
119
269
|
{
|
|
120
270
|
name: 'monitor_tool',
|
|
121
271
|
description: MonitorPlugin.tools![0].description,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type ToolArgsRecord = Record<string, unknown>
|
|
2
|
+
const NESTED_WRAPPER_KEYS = ['input', 'args', 'arguments', 'payload', 'parameters'] as const
|
|
2
3
|
|
|
3
4
|
function parseRecordCandidate(value: unknown): ToolArgsRecord | null {
|
|
4
5
|
if (!value) return null
|
|
@@ -26,22 +27,24 @@ function parseRecordCandidate(value: unknown): ToolArgsRecord | null {
|
|
|
26
27
|
* as either objects or JSON strings.
|
|
27
28
|
*/
|
|
28
29
|
export function normalizeToolInputArgs(rawArgs: ToolArgsRecord): ToolArgsRecord {
|
|
29
|
-
const nestedSources: Array<ToolArgsRecord | null> = [
|
|
30
|
-
parseRecordCandidate(rawArgs.input),
|
|
31
|
-
parseRecordCandidate(rawArgs.args),
|
|
32
|
-
parseRecordCandidate(rawArgs.arguments),
|
|
33
|
-
parseRecordCandidate(rawArgs.payload),
|
|
34
|
-
]
|
|
35
|
-
|
|
36
30
|
const normalized: ToolArgsRecord = {}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
const queue: ToolArgsRecord[] = [rawArgs]
|
|
32
|
+
const visited = new Set<ToolArgsRecord>()
|
|
33
|
+
|
|
34
|
+
while (queue.length > 0) {
|
|
35
|
+
const current = queue.shift()
|
|
36
|
+
if (!current || visited.has(current)) continue
|
|
37
|
+
visited.add(current)
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
for (const key of NESTED_WRAPPER_KEYS) {
|
|
40
|
+
const nested = parseRecordCandidate(current[key])
|
|
41
|
+
if (nested) queue.push(nested)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const [key, value] of Object.entries(current)) {
|
|
45
|
+
if (value === undefined || value === null) continue
|
|
46
|
+
normalized[key] = value
|
|
47
|
+
}
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
return normalized
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import { executeNodesAction } from './openclaw-nodes'
|
|
4
|
+
import type { OpenClawGateway } from '../openclaw-gateway'
|
|
5
|
+
|
|
6
|
+
test('executeNodesAction returns not_connected when no gateway is available', async () => {
|
|
7
|
+
const raw = await executeNodesAction(
|
|
8
|
+
{ action: 'list', profileId: 'gateway-1' },
|
|
9
|
+
{ ensureGatewayConnected: async () => null },
|
|
10
|
+
)
|
|
11
|
+
const result = JSON.parse(raw)
|
|
12
|
+
assert.equal(result.status, 'not_connected')
|
|
13
|
+
assert.match(result.message, /gateway not connected/i)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('executeNodesAction lists nodes against the selected gateway profile', async () => {
|
|
17
|
+
const calls: Array<{ method: string; params: unknown }> = []
|
|
18
|
+
const gateway = {
|
|
19
|
+
rpc: async (method: string, params?: unknown) => {
|
|
20
|
+
calls.push({ method, params })
|
|
21
|
+
return { ts: 1, nodes: [{ nodeId: 'node-1' }] }
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const raw = await executeNodesAction(
|
|
26
|
+
{ action: 'list', profileId: 'gateway-1' },
|
|
27
|
+
{ ensureGatewayConnected: async () => gateway as unknown as OpenClawGateway },
|
|
28
|
+
)
|
|
29
|
+
const result = JSON.parse(raw)
|
|
30
|
+
assert.equal(result.status, 'ok')
|
|
31
|
+
assert.equal(calls[0]?.method, 'node.list')
|
|
32
|
+
assert.deepEqual(calls[0]?.params, { profileId: 'gateway-1' })
|
|
33
|
+
assert.equal(result.result.nodes[0].nodeId, 'node-1')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('executeNodesAction aggregates node and device pairings', async () => {
|
|
37
|
+
const calls: string[] = []
|
|
38
|
+
const gateway = {
|
|
39
|
+
rpc: async (method: string) => {
|
|
40
|
+
calls.push(method)
|
|
41
|
+
if (method === 'node.pair.list') return { pending: [{ requestId: 'node-req-1' }] }
|
|
42
|
+
if (method === 'device.pair.list') return { pending: [{ requestId: 'device-req-1' }], paired: [{ deviceId: 'device-1' }] }
|
|
43
|
+
throw new Error(`Unexpected RPC ${method}`)
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const raw = await executeNodesAction(
|
|
48
|
+
{ action: 'pairings', profileId: 'gateway-1' },
|
|
49
|
+
{ ensureGatewayConnected: async () => gateway as unknown as OpenClawGateway },
|
|
50
|
+
)
|
|
51
|
+
const result = JSON.parse(raw)
|
|
52
|
+
assert.equal(result.status, 'ok')
|
|
53
|
+
assert.deepEqual(calls, ['node.pair.list', 'device.pair.list'])
|
|
54
|
+
assert.equal(result.result.nodePairings.pending[0].requestId, 'node-req-1')
|
|
55
|
+
assert.equal(result.result.devicePairings.paired[0].deviceId, 'device-1')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('executeNodesAction routes device pairing approvals to the device RPC surface', async () => {
|
|
59
|
+
const calls: Array<{ method: string; params: unknown }> = []
|
|
60
|
+
const gateway = {
|
|
61
|
+
rpc: async (method: string, params?: unknown) => {
|
|
62
|
+
calls.push({ method, params })
|
|
63
|
+
return { ok: true }
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const raw = await executeNodesAction(
|
|
68
|
+
{ action: 'approve_pairing', pairingType: 'device', requestId: 'req-1', profileId: 'gateway-1' },
|
|
69
|
+
{ ensureGatewayConnected: async () => gateway as unknown as OpenClawGateway },
|
|
70
|
+
)
|
|
71
|
+
const result = JSON.parse(raw)
|
|
72
|
+
assert.equal(result.status, 'ok')
|
|
73
|
+
assert.equal(calls[0]?.method, 'device.pair.approve')
|
|
74
|
+
assert.deepEqual(calls[0]?.params, { requestId: 'req-1', profileId: 'gateway-1' })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('executeNodesAction forwards notify payloads through node.invoke with a generated idempotency key', async () => {
|
|
78
|
+
const calls: Array<{ method: string; params: unknown }> = []
|
|
79
|
+
const gateway = {
|
|
80
|
+
rpc: async (method: string, params?: unknown) => {
|
|
81
|
+
calls.push({ method, params })
|
|
82
|
+
return { delivered: true }
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const raw = await executeNodesAction(
|
|
87
|
+
{
|
|
88
|
+
action: 'notify',
|
|
89
|
+
profileId: 'gateway-1',
|
|
90
|
+
nodeId: 'node-42',
|
|
91
|
+
message: 'hello from test',
|
|
92
|
+
params: { urgency: 'high' },
|
|
93
|
+
timeoutMs: 5000,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
ensureGatewayConnected: async () => gateway as unknown as OpenClawGateway,
|
|
97
|
+
generateId: () => 'fixed-id',
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
const result = JSON.parse(raw)
|
|
101
|
+
assert.equal(result.status, 'ok')
|
|
102
|
+
assert.equal(calls[0]?.method, 'node.invoke')
|
|
103
|
+
assert.deepEqual(calls[0]?.params, {
|
|
104
|
+
nodeId: 'node-42',
|
|
105
|
+
command: 'notify',
|
|
106
|
+
params: { urgency: 'high', message: 'hello from test' },
|
|
107
|
+
timeoutMs: 5000,
|
|
108
|
+
idempotencyKey: 'fixed-id',
|
|
109
|
+
profileId: 'gateway-1',
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -1,46 +1,105 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
+
import { randomUUID } from 'crypto'
|
|
2
3
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
4
|
import type { ToolBuildContext } from './context'
|
|
4
5
|
import type { Plugin, PluginHooks } from '@/types'
|
|
5
6
|
import { getPluginManager } from '../plugins'
|
|
6
7
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
8
|
+
import { ensureGatewayConnected } from '../openclaw-gateway'
|
|
9
|
+
|
|
10
|
+
interface OpenClawNodesDeps {
|
|
11
|
+
ensureGatewayConnected?: typeof ensureGatewayConnected
|
|
12
|
+
generateId?: () => string
|
|
13
|
+
}
|
|
7
14
|
|
|
8
15
|
/**
|
|
9
16
|
* Core OpenClaw Nodes Execution Logic
|
|
10
17
|
*/
|
|
11
|
-
async function executeNodesAction(args: any) {
|
|
18
|
+
export async function executeNodesAction(args: any, deps: OpenClawNodesDeps = {}) {
|
|
12
19
|
const normalized = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
|
|
13
20
|
const action = normalized.action as string | undefined
|
|
14
21
|
const nodeId = (normalized.nodeId ?? normalized.node_id) as string | undefined
|
|
22
|
+
const deviceId = (normalized.deviceId ?? normalized.device_id) as string | undefined
|
|
23
|
+
const requestId = (normalized.requestId ?? normalized.request_id) as string | undefined
|
|
15
24
|
const message = normalized.message as string | undefined
|
|
16
25
|
const params = normalized.params as Record<string, unknown> | undefined
|
|
26
|
+
const command = (normalized.command ?? params?.command ?? params?.action) as string | undefined
|
|
27
|
+
const pairingType = typeof normalized.pairingType === 'string' ? normalized.pairingType : (typeof normalized.kind === 'string' ? normalized.kind : 'node')
|
|
28
|
+
const profileId = (normalized.profileId ?? normalized.gatewayProfileId ?? normalized.gateway_profile_id) as string | undefined
|
|
29
|
+
const agentId = (normalized.agentId ?? normalized.agent_id) as string | undefined
|
|
30
|
+
const timeoutMs = typeof normalized.timeoutMs === 'number'
|
|
31
|
+
? normalized.timeoutMs
|
|
32
|
+
: (typeof params?.timeoutMs === 'number' ? params.timeoutMs : undefined)
|
|
33
|
+
const ensureGatewayConnectedFn = deps.ensureGatewayConnected ?? ensureGatewayConnected
|
|
34
|
+
const generateId = deps.generateId ?? randomUUID
|
|
17
35
|
try {
|
|
18
|
-
const {
|
|
19
|
-
|
|
20
|
-
if (!openclawConnectors.length) {
|
|
36
|
+
const gateway = await ensureGatewayConnectedFn({ profileId, agentId })
|
|
37
|
+
if (!gateway) {
|
|
21
38
|
return JSON.stringify({
|
|
22
39
|
status: 'not_connected',
|
|
23
|
-
message: '
|
|
24
|
-
hint: '
|
|
40
|
+
message: 'OpenClaw gateway not connected.',
|
|
41
|
+
hint: 'Connect an OpenClaw gateway profile in Providers, then retry.',
|
|
25
42
|
})
|
|
26
43
|
}
|
|
27
|
-
|
|
28
|
-
if (
|
|
44
|
+
|
|
45
|
+
if (action === 'list') {
|
|
46
|
+
const result = await gateway.rpc('node.list', { profileId })
|
|
47
|
+
return JSON.stringify({ status: 'ok', action, result })
|
|
48
|
+
}
|
|
49
|
+
if (action === 'describe') {
|
|
50
|
+
if (!nodeId) return JSON.stringify({ status: 'error', error: 'nodeId is required for describe.' })
|
|
51
|
+
const result = await gateway.rpc('node.describe', { nodeId, profileId })
|
|
52
|
+
return JSON.stringify({ status: 'ok', action, nodeId, result })
|
|
53
|
+
}
|
|
54
|
+
if (action === 'pairings') {
|
|
55
|
+
const [nodePairings, devicePairings] = await Promise.all([
|
|
56
|
+
gateway.rpc('node.pair.list', { profileId }),
|
|
57
|
+
gateway.rpc('device.pair.list', { profileId }),
|
|
58
|
+
])
|
|
29
59
|
return JSON.stringify({
|
|
30
|
-
status: '
|
|
31
|
-
|
|
32
|
-
|
|
60
|
+
status: 'ok',
|
|
61
|
+
action,
|
|
62
|
+
result: {
|
|
63
|
+
nodePairings,
|
|
64
|
+
devicePairings,
|
|
65
|
+
},
|
|
33
66
|
})
|
|
34
67
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
68
|
+
if (action === 'approve_pairing') {
|
|
69
|
+
if (!requestId) return JSON.stringify({ status: 'error', error: 'requestId is required for approve_pairing.' })
|
|
70
|
+
const method = pairingType === 'device' ? 'device.pair.approve' : 'node.pair.approve'
|
|
71
|
+
const result = await gateway.rpc(method, { requestId, profileId })
|
|
72
|
+
return JSON.stringify({ status: 'ok', action, pairingType, requestId, result })
|
|
73
|
+
}
|
|
74
|
+
if (action === 'reject_pairing') {
|
|
75
|
+
if (!requestId) return JSON.stringify({ status: 'error', error: 'requestId is required for reject_pairing.' })
|
|
76
|
+
const method = pairingType === 'device' ? 'device.pair.reject' : 'node.pair.reject'
|
|
77
|
+
const result = await gateway.rpc(method, { requestId, profileId })
|
|
78
|
+
return JSON.stringify({ status: 'ok', action, pairingType, requestId, result })
|
|
38
79
|
}
|
|
39
|
-
if (action === '
|
|
40
|
-
return JSON.stringify({ status: '
|
|
80
|
+
if (action === 'remove_device') {
|
|
81
|
+
if (!deviceId) return JSON.stringify({ status: 'error', error: 'deviceId is required for remove_device.' })
|
|
82
|
+
const result = await gateway.rpc('device.pair.remove', { deviceId, profileId })
|
|
83
|
+
return JSON.stringify({ status: 'ok', action, deviceId, result })
|
|
41
84
|
}
|
|
42
|
-
if (action === 'invoke') {
|
|
43
|
-
return JSON.stringify({ status: '
|
|
85
|
+
if (action === 'notify' || action === 'invoke') {
|
|
86
|
+
if (!nodeId) return JSON.stringify({ status: 'error', error: 'nodeId is required for invoke.' })
|
|
87
|
+
const invokeCommand = typeof command === 'string' && command.trim()
|
|
88
|
+
? command.trim()
|
|
89
|
+
: (action === 'notify' ? 'notify' : '')
|
|
90
|
+
if (!invokeCommand) return JSON.stringify({ status: 'error', error: 'command is required for invoke.' })
|
|
91
|
+
const invokeParams = action === 'notify'
|
|
92
|
+
? { ...(params || {}), message }
|
|
93
|
+
: (params || {})
|
|
94
|
+
const result = await gateway.rpc('node.invoke', {
|
|
95
|
+
nodeId,
|
|
96
|
+
command: invokeCommand,
|
|
97
|
+
params: invokeParams,
|
|
98
|
+
timeoutMs,
|
|
99
|
+
idempotencyKey: generateId(),
|
|
100
|
+
profileId,
|
|
101
|
+
})
|
|
102
|
+
return JSON.stringify({ status: 'ok', action, nodeId, command: invokeCommand, result })
|
|
44
103
|
}
|
|
45
104
|
|
|
46
105
|
return JSON.stringify({ status: 'error', error: `Unknown nodes action "${action}".` })
|
|
@@ -63,10 +122,17 @@ const NodesPlugin: Plugin = {
|
|
|
63
122
|
parameters: {
|
|
64
123
|
type: 'object',
|
|
65
124
|
properties: {
|
|
66
|
-
action: { type: 'string', enum: ['list', 'notify', 'invoke'] },
|
|
125
|
+
action: { type: 'string', enum: ['list', 'describe', 'pairings', 'approve_pairing', 'reject_pairing', 'remove_device', 'notify', 'invoke'] },
|
|
67
126
|
nodeId: { type: 'string' },
|
|
127
|
+
deviceId: { type: 'string' },
|
|
128
|
+
requestId: { type: 'string' },
|
|
129
|
+
pairingType: { type: 'string', enum: ['node', 'device'] },
|
|
130
|
+
profileId: { type: 'string' },
|
|
131
|
+
agentId: { type: 'string' },
|
|
132
|
+
command: { type: 'string' },
|
|
68
133
|
message: { type: 'string' },
|
|
69
|
-
params: { type: 'object' }
|
|
134
|
+
params: { type: 'object' },
|
|
135
|
+
timeoutMs: { type: 'number' },
|
|
70
136
|
},
|
|
71
137
|
required: ['action']
|
|
72
138
|
},
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { normalizePlatformActionArgs } from './platform'
|
|
4
|
+
|
|
5
|
+
describe('normalizePlatformActionArgs', () => {
|
|
6
|
+
it('packs top-level create fields into data', () => {
|
|
7
|
+
const out = normalizePlatformActionArgs({
|
|
8
|
+
resource: 'tasks',
|
|
9
|
+
action: 'create',
|
|
10
|
+
title: 'Write docs',
|
|
11
|
+
agentId: 'default',
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
assert.equal(out.resource, 'tasks')
|
|
15
|
+
assert.equal(out.action, 'create')
|
|
16
|
+
assert.deepEqual(JSON.parse(String(out.data)), {
|
|
17
|
+
title: 'Write docs',
|
|
18
|
+
agentId: 'default',
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('merges object data with top-level overrides', () => {
|
|
23
|
+
const out = normalizePlatformActionArgs({
|
|
24
|
+
resource: 'tasks',
|
|
25
|
+
action: 'create',
|
|
26
|
+
data: { title: 'Old title', agentId: 'coder' },
|
|
27
|
+
title: 'New title',
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
assert.deepEqual(JSON.parse(String(out.data)), {
|
|
31
|
+
title: 'New title',
|
|
32
|
+
agentId: 'coder',
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('normalizes legacy resources envelope with parameters payload', () => {
|
|
37
|
+
const out = normalizePlatformActionArgs({
|
|
38
|
+
input: JSON.stringify({
|
|
39
|
+
resources: [
|
|
40
|
+
{
|
|
41
|
+
resource: 'tasks',
|
|
42
|
+
action: 'create',
|
|
43
|
+
parameters: {
|
|
44
|
+
title: 'Legacy task',
|
|
45
|
+
assigned_agent: 'default',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
}),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
assert.equal(out.resource, 'tasks')
|
|
53
|
+
assert.equal(out.action, 'create')
|
|
54
|
+
assert.deepEqual(JSON.parse(String(out.data)), {
|
|
55
|
+
title: 'Legacy task',
|
|
56
|
+
assigned_agent: 'default',
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('normalizes singular resource names and resource payload objects', () => {
|
|
61
|
+
const out = normalizePlatformActionArgs({
|
|
62
|
+
input: JSON.stringify({
|
|
63
|
+
resource: 'task',
|
|
64
|
+
action: 'create',
|
|
65
|
+
task: {
|
|
66
|
+
title: 'Legacy singular task',
|
|
67
|
+
assigned_to: 'default',
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
assert.equal(out.resource, 'tasks')
|
|
73
|
+
assert.equal(out.action, 'create')
|
|
74
|
+
assert.deepEqual(JSON.parse(String(out.data)), {
|
|
75
|
+
title: 'Legacy singular task',
|
|
76
|
+
assigned_to: 'default',
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('normalizes legacy backlog task resource names to tasks', () => {
|
|
81
|
+
const out = normalizePlatformActionArgs({
|
|
82
|
+
input: JSON.stringify({
|
|
83
|
+
resource: 'backlog_task',
|
|
84
|
+
action: 'create',
|
|
85
|
+
backlog_task: {
|
|
86
|
+
title: 'Legacy backlog task',
|
|
87
|
+
description: 'Keep the intended task payload',
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
assert.equal(out.resource, 'tasks')
|
|
93
|
+
assert.equal(out.action, 'create')
|
|
94
|
+
assert.deepEqual(JSON.parse(String(out.data)), {
|
|
95
|
+
title: 'Legacy backlog task',
|
|
96
|
+
description: 'Keep the intended task payload',
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('normalizes resources entries that use type instead of resource', () => {
|
|
101
|
+
const out = normalizePlatformActionArgs({
|
|
102
|
+
input: JSON.stringify({
|
|
103
|
+
action: 'create',
|
|
104
|
+
resources: [
|
|
105
|
+
{
|
|
106
|
+
type: 'task',
|
|
107
|
+
parameters: {
|
|
108
|
+
title: 'Typed task resource',
|
|
109
|
+
description: 'Created through a typed resources envelope',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
}),
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
assert.equal(out.resource, 'tasks')
|
|
117
|
+
assert.equal(out.action, 'create')
|
|
118
|
+
assert.deepEqual(JSON.parse(String(out.data)), {
|
|
119
|
+
title: 'Typed task resource',
|
|
120
|
+
description: 'Created through a typed resources envelope',
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('infers schedules resource from create_schedule style actions', () => {
|
|
125
|
+
const out = normalizePlatformActionArgs({
|
|
126
|
+
input: JSON.stringify({
|
|
127
|
+
action: 'create_schedule',
|
|
128
|
+
data: {
|
|
129
|
+
name: 'Surgery check-in',
|
|
130
|
+
scheduleType: 'once',
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
assert.equal(out.resource, 'schedules')
|
|
136
|
+
assert.equal(out.action, 'create')
|
|
137
|
+
assert.deepEqual(JSON.parse(String(out.data)), {
|
|
138
|
+
name: 'Surgery check-in',
|
|
139
|
+
scheduleType: 'once',
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
})
|