@swarmclawai/swarmclaw 0.4.0 → 0.4.5
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 +13 -2
- package/next.config.ts +8 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +20 -21
- package/src/app/api/agents/[id]/thread/route.ts +2 -2
- package/src/app/api/agents/route.ts +3 -2
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +10 -3
- package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
- package/src/app/api/connectors/route.ts +6 -3
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -2
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +2 -2
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- package/src/app/api/openclaw/directory/route.ts +26 -0
- package/src/app/api/openclaw/discover/route.ts +61 -0
- package/src/app/api/openclaw/sync/route.ts +30 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -15
- package/src/app/api/providers/route.ts +2 -2
- package/src/app/api/schedules/[id]/route.ts +16 -18
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +2 -2
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +2 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
- package/src/app/api/sessions/[id]/messages/route.ts +2 -1
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +2 -1
- package/src/app/api/sessions/route.ts +2 -2
- package/src/app/api/skills/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +2 -2
- package/src/app/api/tasks/[id]/approve/route.ts +2 -1
- package/src/app/api/tasks/[id]/route.ts +6 -5
- package/src/app/api/tasks/route.ts +2 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- package/src/app/api/webhooks/[id]/route.ts +29 -31
- package/src/app/api/webhooks/route.ts +2 -2
- package/src/app/page.tsx +3 -24
- package/src/cli/index.js +28 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/spec.js +2 -0
- package/src/components/agents/agent-list.tsx +3 -1
- package/src/components/agents/agent-sheet.tsx +116 -14
- package/src/components/chat/chat-area.tsx +27 -4
- package/src/components/chat/chat-header.tsx +141 -29
- package/src/components/chat/tool-call-bubble.tsx +9 -3
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +6 -2
- package/src/components/connectors/connector-sheet.tsx +31 -7
- package/src/components/layout/app-layout.tsx +47 -25
- package/src/components/projects/project-list.tsx +122 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/schedules/schedule-list.tsx +3 -1
- package/src/components/sessions/new-session-sheet.tsx +6 -6
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/shared/connector-platform-icon.tsx +4 -0
- package/src/components/shared/settings/section-heartbeat.tsx +1 -1
- package/src/components/shared/settings/section-orchestrator.tsx +1 -2
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +73 -0
- package/src/components/skills/skill-list.tsx +2 -1
- package/src/components/tasks/task-list.tsx +5 -2
- package/src/hooks/use-continuous-speech.ts +144 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/lib/id.ts +6 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +14 -1
- package/src/lib/providers/index.ts +6 -0
- package/src/lib/providers/ollama.ts +9 -1
- package/src/lib/providers/openai.ts +9 -1
- package/src/lib/providers/openclaw.ts +11 -0
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +38 -4
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
- package/src/lib/server/connectors/bluebubbles.ts +357 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +46 -7
- package/src/lib/server/connectors/manager.ts +392 -3
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +64 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +1 -1
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/main-agent-loop.ts +6 -6
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-sync.ts +496 -0
- package/src/lib/server/orchestrator-lg.ts +30 -9
- package/src/lib/server/orchestrator.ts +4 -4
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +22 -10
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +2 -2
- package/src/lib/server/session-tools/connector.ts +51 -4
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +3 -3
- package/src/lib/server/session-tools/file.ts +176 -3
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +33 -0
- package/src/lib/server/session-tools/search-providers.ts +270 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/web.ts +47 -66
- package/src/lib/server/storage.ts +12 -0
- package/src/lib/server/stream-agent-chat.ts +29 -0
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/tool-definitions.ts +5 -3
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/view-routes.ts +28 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +28 -1
- package/src/stores/use-chat-store.ts +9 -1
- package/src/types/index.ts +27 -2
package/src/lib/server/queue.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
2
|
import { loadTasks, saveTasks, loadQueue, saveQueue, loadAgents, loadSchedules, saveSchedules, loadSessions, saveSessions, loadSettings } from './storage'
|
|
3
3
|
import { notify } from './ws-hub'
|
|
4
4
|
import { WORKSPACE_DIR } from './data-dir'
|
|
@@ -155,7 +155,11 @@ function notifyMainChatScheduleResult(task: BoardTask): void {
|
|
|
155
155
|
const fallbackText = runSession ? latestAssistantText(runSession) : ''
|
|
156
156
|
|
|
157
157
|
// Zod-validated structured extraction: one pass to get summary + all artifacts
|
|
158
|
-
const taskResult = extractTaskResult(
|
|
158
|
+
const taskResult = extractTaskResult(
|
|
159
|
+
runSession,
|
|
160
|
+
task.result || fallbackText || null,
|
|
161
|
+
{ sinceTime: typeof task.startedAt === 'number' ? task.startedAt : null },
|
|
162
|
+
)
|
|
159
163
|
const resultBody = formatResultBody(taskResult)
|
|
160
164
|
|
|
161
165
|
const statusLabel = task.status === 'completed' ? 'completed' : 'failed'
|
|
@@ -224,7 +228,11 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
|
|
|
224
228
|
const runSessionId = typeof task.sessionId === 'string' ? task.sessionId : ''
|
|
225
229
|
const runSession = runSessionId ? sessions[runSessionId] : null
|
|
226
230
|
const fallbackText = runSession ? latestAssistantText(runSession) : ''
|
|
227
|
-
const taskResult = extractTaskResult(
|
|
231
|
+
const taskResult = extractTaskResult(
|
|
232
|
+
runSession,
|
|
233
|
+
task.result || fallbackText || null,
|
|
234
|
+
{ sinceTime: typeof task.startedAt === 'number' ? task.startedAt : null },
|
|
235
|
+
)
|
|
228
236
|
const resultBody = formatResultBody(taskResult)
|
|
229
237
|
|
|
230
238
|
const statusLabel = task.status === 'completed' ? 'completed' : 'failed'
|
|
@@ -374,7 +382,7 @@ export function validateCompletedTasksQueue() {
|
|
|
374
382
|
task.updatedAt = now
|
|
375
383
|
if (!task.comments) task.comments = []
|
|
376
384
|
task.comments.push({
|
|
377
|
-
id:
|
|
385
|
+
id: genId(),
|
|
378
386
|
author: 'System',
|
|
379
387
|
text: `Task auto-failed completed-queue validation.\n\n${validation.reasons.map((r) => `- ${r}`).join('\n')}`,
|
|
380
388
|
createdAt: now,
|
|
@@ -413,7 +421,7 @@ function scheduleRetryOrDeadLetter(task: BoardTask, reason: string): 'retry' | '
|
|
|
413
421
|
task.error = `Retry scheduled after failure: ${reason}`.slice(0, 500)
|
|
414
422
|
if (!task.comments) task.comments = []
|
|
415
423
|
task.comments.push({
|
|
416
|
-
id:
|
|
424
|
+
id: genId(),
|
|
417
425
|
author: 'System',
|
|
418
426
|
text: `Attempt ${task.attempts}/${task.maxAttempts} failed. Retrying in ${delaySec}s.\n\nReason: ${reason}`,
|
|
419
427
|
createdAt: now,
|
|
@@ -428,7 +436,7 @@ function scheduleRetryOrDeadLetter(task: BoardTask, reason: string): 'retry' | '
|
|
|
428
436
|
task.error = `Dead-lettered after ${task.attempts}/${task.maxAttempts} attempts: ${reason}`.slice(0, 500)
|
|
429
437
|
if (!task.comments) task.comments = []
|
|
430
438
|
task.comments.push({
|
|
431
|
-
id:
|
|
439
|
+
id: genId(),
|
|
432
440
|
author: 'System',
|
|
433
441
|
text: `Task moved to dead-letter after ${task.attempts}/${task.maxAttempts} attempts.\n\nReason: ${reason}`,
|
|
434
442
|
createdAt: now,
|
|
@@ -610,7 +618,11 @@ export async function processNext() {
|
|
|
610
618
|
applyTaskPolicyDefaults(t2[taskId])
|
|
611
619
|
// Structured extraction: Zod-validated result with typed artifacts
|
|
612
620
|
const runSessions = loadSessions()
|
|
613
|
-
const taskResult = extractTaskResult(
|
|
621
|
+
const taskResult = extractTaskResult(
|
|
622
|
+
runSessions[sessionId],
|
|
623
|
+
result || null,
|
|
624
|
+
{ sinceTime: typeof t2[taskId].startedAt === 'number' ? t2[taskId].startedAt : null },
|
|
625
|
+
)
|
|
614
626
|
const enrichedResult = formatResultBody(taskResult)
|
|
615
627
|
t2[taskId].result = enrichedResult.slice(0, 4000) || null
|
|
616
628
|
t2[taskId].updatedAt = Date.now()
|
|
@@ -636,7 +648,7 @@ export async function processNext() {
|
|
|
636
648
|
updatedAt: now,
|
|
637
649
|
}
|
|
638
650
|
t2[taskId].comments!.push({
|
|
639
|
-
id:
|
|
651
|
+
id: genId(),
|
|
640
652
|
author: agent.name,
|
|
641
653
|
agentId: agent.id,
|
|
642
654
|
text: `Task completed.\n\n${result?.slice(0, 1000) || 'No summary provided.'}`,
|
|
@@ -647,7 +659,7 @@ export async function processNext() {
|
|
|
647
659
|
const retryState = scheduleRetryOrDeadLetter(t2[taskId], failureReason)
|
|
648
660
|
t2[taskId].completedAt = retryState === 'dead_lettered' ? null : t2[taskId].completedAt
|
|
649
661
|
t2[taskId].comments!.push({
|
|
650
|
-
id:
|
|
662
|
+
id: genId(),
|
|
651
663
|
author: agent.name,
|
|
652
664
|
agentId: agent.id,
|
|
653
665
|
text: `Task failed validation and was not marked completed.\n\n${validation.reasons.map((r) => `- ${r}`).join('\n')}`,
|
|
@@ -739,7 +751,7 @@ export async function processNext() {
|
|
|
739
751
|
const isRepeatError = lastComment?.agentId === agent.id && lastComment?.text.startsWith('Task failed')
|
|
740
752
|
if (!isRepeatError) {
|
|
741
753
|
t2[taskId].comments!.push({
|
|
742
|
-
id:
|
|
754
|
+
id: genId(),
|
|
743
755
|
author: agent.name,
|
|
744
756
|
agentId: agent.id,
|
|
745
757
|
text: 'Task failed — see error details above.',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
2
|
import { loadSchedules, saveSchedules, loadAgents, loadTasks, saveTasks } from './storage'
|
|
3
3
|
import { enqueueTask } from './queue'
|
|
4
4
|
import { CronExpressionParser } from 'cron-parser'
|
|
@@ -157,7 +157,7 @@ async function tick() {
|
|
|
157
157
|
prev.runNumber = schedule.runNumber
|
|
158
158
|
} else {
|
|
159
159
|
// Create a new linked task (first run or previous task still in-flight)
|
|
160
|
-
taskId =
|
|
160
|
+
taskId = genId()
|
|
161
161
|
tasks[taskId] = {
|
|
162
162
|
id: taskId,
|
|
163
163
|
title: `[Sched] ${schedule.name} (run #${schedule.runNumber})`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
2
|
import { loadSessions, saveSessions } from './storage'
|
|
3
3
|
|
|
4
4
|
export type MailboxStatus = 'new' | 'ack'
|
|
@@ -58,7 +58,7 @@ export function sendMailboxEnvelope(input: {
|
|
|
58
58
|
? Math.max(0, Math.min(7 * 24 * 3600, Math.trunc(input.ttlSec)))
|
|
59
59
|
: null
|
|
60
60
|
const envelope: MailboxEnvelope = {
|
|
61
|
-
id:
|
|
61
|
+
id: genId(6),
|
|
62
62
|
type: (input.type || 'message').trim() || 'message',
|
|
63
63
|
payload: String(input.payload || ''),
|
|
64
64
|
fromSessionId: input.fromSessionId || null,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
2
|
import type { SSEEvent } from '@/types'
|
|
3
3
|
import { active, loadSessions } from './storage'
|
|
4
4
|
import { executeSessionChatTurn, type ExecuteChatTurnResult } from './chat-execution'
|
|
@@ -420,7 +420,7 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
|
|
|
420
420
|
}
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
-
const runId =
|
|
423
|
+
const runId = genId(8)
|
|
424
424
|
const run: SessionRunRecord = {
|
|
425
425
|
id: runId,
|
|
426
426
|
sessionId: input.sessionId,
|
|
@@ -58,6 +58,10 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
|
|
|
58
58
|
const outbound = connector.config?.outboundJid?.trim()
|
|
59
59
|
if (outbound) channelId = outbound
|
|
60
60
|
}
|
|
61
|
+
if (!channelId) {
|
|
62
|
+
const outbound = connector.config?.outboundTarget?.trim()
|
|
63
|
+
if (outbound) channelId = outbound
|
|
64
|
+
}
|
|
61
65
|
if (!channelId) {
|
|
62
66
|
const recentChannelId = getConnectorRecentChannelId(selected.id)
|
|
63
67
|
if (recentChannelId) channelId = recentChannelId
|
|
@@ -67,7 +71,11 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
|
|
|
67
71
|
if (allowed.length) channelId = allowed[0]
|
|
68
72
|
}
|
|
69
73
|
if (!channelId) {
|
|
70
|
-
|
|
74
|
+
const allowed = connector.config?.allowFrom?.split(',').map((s: string) => s.trim()).filter(Boolean) || []
|
|
75
|
+
if (allowed.length) channelId = allowed[0]
|
|
76
|
+
}
|
|
77
|
+
if (!channelId) {
|
|
78
|
+
return `Error: no target recipient configured. Provide "to", or set connector config "outboundJid"/"allowedJids"/"outboundTarget"/"allowFrom".`
|
|
71
79
|
}
|
|
72
80
|
if (connector.platform === 'whatsapp') {
|
|
73
81
|
channelId = normalizeWhatsAppTarget(channelId)
|
|
@@ -93,6 +101,45 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
|
|
|
93
101
|
})
|
|
94
102
|
}
|
|
95
103
|
|
|
104
|
+
if (action === 'message_react' || action === 'message_edit' || action === 'message_pin' || action === 'message_delete') {
|
|
105
|
+
if (!connectorId) return 'Error: connectorId is required for rich messaging actions.'
|
|
106
|
+
const { getRunningInstance } = await import('../connectors/manager')
|
|
107
|
+
const inst = getRunningInstance(connectorId)
|
|
108
|
+
if (!inst) return `Error: connector "${connectorId}" is not running.`
|
|
109
|
+
|
|
110
|
+
const targetChannel = to?.trim() || ''
|
|
111
|
+
const targetMessageId = message?.trim() || ''
|
|
112
|
+
if (!targetMessageId) return 'Error: message parameter (used as messageId) is required for rich messaging actions.'
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
if (action === 'message_react') {
|
|
116
|
+
if (!inst.sendReaction) return 'Error: this connector does not support reactions.'
|
|
117
|
+
const emoji = caption?.trim() || '👍'
|
|
118
|
+
await inst.sendReaction(targetChannel, targetMessageId, emoji)
|
|
119
|
+
return JSON.stringify({ status: 'reacted', connectorId, messageId: targetMessageId, emoji })
|
|
120
|
+
}
|
|
121
|
+
if (action === 'message_edit') {
|
|
122
|
+
if (!inst.editMessage) return 'Error: this connector does not support message editing.'
|
|
123
|
+
const newText = caption?.trim() || ''
|
|
124
|
+
if (!newText) return 'Error: caption (new text) is required for message_edit.'
|
|
125
|
+
await inst.editMessage(targetChannel, targetMessageId, newText)
|
|
126
|
+
return JSON.stringify({ status: 'edited', connectorId, messageId: targetMessageId })
|
|
127
|
+
}
|
|
128
|
+
if (action === 'message_delete') {
|
|
129
|
+
if (!inst.deleteMessage) return 'Error: this connector does not support message deletion.'
|
|
130
|
+
await inst.deleteMessage(targetChannel, targetMessageId)
|
|
131
|
+
return JSON.stringify({ status: 'deleted', connectorId, messageId: targetMessageId })
|
|
132
|
+
}
|
|
133
|
+
if (action === 'message_pin') {
|
|
134
|
+
if (!inst.pinMessage) return 'Error: this connector does not support message pinning.'
|
|
135
|
+
await inst.pinMessage(targetChannel, targetMessageId)
|
|
136
|
+
return JSON.stringify({ status: 'pinned', connectorId, messageId: targetMessageId })
|
|
137
|
+
}
|
|
138
|
+
} catch (err: any) {
|
|
139
|
+
return `Error: ${err.message || String(err)}`
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
96
143
|
return 'Unknown action. Use list_running, list_targets, or send.'
|
|
97
144
|
} catch (err: any) {
|
|
98
145
|
return `Error: ${err.message || String(err)}`
|
|
@@ -100,11 +147,11 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
|
|
|
100
147
|
},
|
|
101
148
|
{
|
|
102
149
|
name: 'connector_message_tool',
|
|
103
|
-
description: 'Send proactive outbound messages
|
|
150
|
+
description: 'Send proactive outbound messages and perform rich messaging actions through running connectors. Supports listing running connectors/targets, sending text/media, and rich messaging (react, edit, delete, pin). For rich actions: connectorId + message (as messageId) required; caption carries emoji for react or new text for edit.',
|
|
104
151
|
schema: z.object({
|
|
105
|
-
action: z.enum(['list_running', 'list_targets', 'send']).describe('connector messaging action'),
|
|
152
|
+
action: z.enum(['list_running', 'list_targets', 'send', 'message_react', 'message_edit', 'message_delete', 'message_pin']).describe('connector messaging action'),
|
|
106
153
|
connectorId: z.string().optional().describe('Optional connector id. Defaults to the first running connector (or first for selected platform).'),
|
|
107
|
-
platform: z.string().optional().describe('Optional platform filter (whatsapp, telegram, slack, discord).'),
|
|
154
|
+
platform: z.string().optional().describe('Optional platform filter (whatsapp, telegram, slack, discord, bluebubbles, etc.).'),
|
|
108
155
|
to: z.string().optional().describe('Target channel id / recipient. For WhatsApp, phone number or full JID.'),
|
|
109
156
|
message: z.string().optional().describe('Message text to send (required for send action).'),
|
|
110
157
|
imageUrl: z.string().optional().describe('Optional public image URL to attach/send where platform supports media.'),
|
|
@@ -2,7 +2,7 @@ import { z } from 'zod'
|
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
3
|
import fs from 'fs'
|
|
4
4
|
import path from 'path'
|
|
5
|
-
import
|
|
5
|
+
import { genId } from '@/lib/id'
|
|
6
6
|
import { spawnSync } from 'child_process'
|
|
7
7
|
import * as cheerio from 'cheerio'
|
|
8
8
|
import {
|
|
@@ -342,7 +342,7 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
342
342
|
})
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
|
-
const newId =
|
|
345
|
+
const newId = genId()
|
|
346
346
|
const entry = {
|
|
347
347
|
id: newId,
|
|
348
348
|
...parsed,
|
|
@@ -565,7 +565,7 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
565
565
|
const content = trimDocumentContent(extracted.text)
|
|
566
566
|
if (!content) return 'Error: extracted document text is empty.'
|
|
567
567
|
|
|
568
|
-
const docId =
|
|
568
|
+
const docId = genId(6)
|
|
569
569
|
const now = Date.now()
|
|
570
570
|
const parsedMetadata = metadata && typeof metadata === 'string'
|
|
571
571
|
? (() => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
-
import
|
|
3
|
+
import { genId } from '@/lib/id'
|
|
4
4
|
import { spawn, spawnSync } from 'child_process'
|
|
5
5
|
import { loadAgents, loadTasks, upsertTask } from '../storage'
|
|
6
6
|
import { log } from '../logger'
|
|
@@ -640,7 +640,7 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
640
640
|
}
|
|
641
641
|
if (!target) return `Error: Agent "${targetAgentId}" not found. Use the agent directory in your system prompt to find valid agent IDs.`
|
|
642
642
|
|
|
643
|
-
const taskId =
|
|
643
|
+
const taskId = genId()
|
|
644
644
|
const now = Date.now()
|
|
645
645
|
const newTask = {
|
|
646
646
|
id: taskId,
|
|
@@ -653,7 +653,7 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
653
653
|
createdAt: now,
|
|
654
654
|
updatedAt: now,
|
|
655
655
|
comments: [{
|
|
656
|
-
id:
|
|
656
|
+
id: genId(),
|
|
657
657
|
author: agents[ctx.agentId!]?.name || 'Agent',
|
|
658
658
|
agentId: ctx.agentId!,
|
|
659
659
|
text: `Delegated from ${agents[ctx.agentId!]?.name || ctx.agentId}`,
|
|
@@ -44,10 +44,15 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
44
44
|
if (canWriteFiles) {
|
|
45
45
|
tools.push(
|
|
46
46
|
tool(
|
|
47
|
-
async ({ filePath, content }) => {
|
|
47
|
+
async ({ filePath, content, encoding }) => {
|
|
48
48
|
try {
|
|
49
49
|
const resolved = safePath(bctx.cwd, filePath)
|
|
50
50
|
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
51
|
+
if (encoding === 'base64') {
|
|
52
|
+
const buf = Buffer.from(content, 'base64')
|
|
53
|
+
fs.writeFileSync(resolved, buf)
|
|
54
|
+
return `File written: ${filePath} (${buf.length} bytes, binary)`
|
|
55
|
+
}
|
|
51
56
|
fs.writeFileSync(resolved, content, 'utf-8')
|
|
52
57
|
return `File written: ${filePath} (${content.length} bytes)`
|
|
53
58
|
} catch (err: any) {
|
|
@@ -56,10 +61,11 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
56
61
|
},
|
|
57
62
|
{
|
|
58
63
|
name: 'write_file',
|
|
59
|
-
description: 'Write content to a file in the session working directory. Creates directories if needed.',
|
|
64
|
+
description: 'Write content to a file in the session working directory. Creates directories if needed. For PDFs and styled reports, use the create_document tool instead. For other binary files (Excel, images, zip, etc.), set encoding to "base64" and pass base64-encoded content.',
|
|
60
65
|
schema: z.object({
|
|
61
66
|
filePath: z.string().describe('Relative path to the file'),
|
|
62
|
-
content: z.string().describe('The content to write'),
|
|
67
|
+
content: z.string().describe('The content to write. For binary files, this must be a base64-encoded string.'),
|
|
68
|
+
encoding: z.enum(['utf-8', 'base64']).optional().describe('Encoding of the content. Use "base64" for binary files like PDF, Excel, images, zip archives. Defaults to "utf-8" for plain text.'),
|
|
63
69
|
}),
|
|
64
70
|
},
|
|
65
71
|
),
|
|
@@ -229,6 +235,173 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
229
235
|
)
|
|
230
236
|
}
|
|
231
237
|
|
|
238
|
+
if (canSendFiles || canWriteFiles) {
|
|
239
|
+
// create_document: markdown → pdf / html / png / jpg
|
|
240
|
+
tools.push(
|
|
241
|
+
tool(
|
|
242
|
+
async ({ content, title, filename, format }) => {
|
|
243
|
+
try {
|
|
244
|
+
const fmt = format || 'pdf'
|
|
245
|
+
const { marked } = await import('marked')
|
|
246
|
+
const html = await marked.parse(content)
|
|
247
|
+
const safeTitle = (title || 'Document').replace(/</g, '<')
|
|
248
|
+
const fullHtml = `<!DOCTYPE html>
|
|
249
|
+
<html><head><meta charset="utf-8"><title>${safeTitle}</title>
|
|
250
|
+
<style>
|
|
251
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:800px;margin:40px auto;padding:0 20px;color:#1a1a1a;line-height:1.6}
|
|
252
|
+
h1{font-size:28px;border-bottom:2px solid #e5e7eb;padding-bottom:8px}
|
|
253
|
+
h2{font-size:22px;margin-top:32px}
|
|
254
|
+
h3{font-size:18px;margin-top:24px}
|
|
255
|
+
pre{background:#f3f4f6;padding:16px;border-radius:8px;overflow-x:auto;font-size:13px}
|
|
256
|
+
code{background:#f3f4f6;padding:2px 6px;border-radius:4px;font-size:13px}
|
|
257
|
+
pre code{background:none;padding:0}
|
|
258
|
+
table{border-collapse:collapse;width:100%}
|
|
259
|
+
th,td{border:1px solid #d1d5db;padding:8px 12px;text-align:left}
|
|
260
|
+
th{background:#f9fafb;font-weight:600}
|
|
261
|
+
blockquote{border-left:4px solid #d1d5db;margin:16px 0;padding:8px 16px;color:#4b5563}
|
|
262
|
+
img{max-width:100%}
|
|
263
|
+
</style></head><body>${html}</body></html>`
|
|
264
|
+
|
|
265
|
+
const defaultBase = (title || 'document').replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
266
|
+
|
|
267
|
+
if (fmt === 'html') {
|
|
268
|
+
const outName = filename || `${defaultBase}.html`
|
|
269
|
+
const resolved = safePath(bctx.cwd, outName)
|
|
270
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
271
|
+
fs.writeFileSync(resolved, fullHtml, 'utf-8')
|
|
272
|
+
return `HTML document created: ${outName} (${fullHtml.length} bytes)`
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const { chromium } = await import('playwright')
|
|
276
|
+
const browser = await chromium.launch({ headless: true })
|
|
277
|
+
try {
|
|
278
|
+
const page = await browser.newPage()
|
|
279
|
+
await page.setContent(fullHtml, { waitUntil: 'networkidle' })
|
|
280
|
+
|
|
281
|
+
if (fmt === 'pdf') {
|
|
282
|
+
const outName = filename || `${defaultBase}.pdf`
|
|
283
|
+
const resolved = safePath(bctx.cwd, outName)
|
|
284
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
285
|
+
await page.pdf({ path: resolved, format: 'A4', margin: { top: '40px', bottom: '40px', left: '40px', right: '40px' }, printBackground: true })
|
|
286
|
+
return `PDF created: ${outName}`
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// png or jpg screenshot
|
|
290
|
+
const ext = fmt === 'jpg' ? 'jpeg' : 'png'
|
|
291
|
+
const outName = filename || `${defaultBase}.${fmt}`
|
|
292
|
+
const resolved = safePath(bctx.cwd, outName)
|
|
293
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
294
|
+
await page.screenshot({ path: resolved, type: ext, fullPage: true })
|
|
295
|
+
const size = fs.statSync(resolved).size
|
|
296
|
+
return `Image created: ${outName} (${(size / 1024).toFixed(1)} KB)`
|
|
297
|
+
} finally {
|
|
298
|
+
await browser.close()
|
|
299
|
+
}
|
|
300
|
+
} catch (err: any) {
|
|
301
|
+
return `Error creating document: ${err.message}`
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
name: 'create_document',
|
|
306
|
+
description: 'Create a document from markdown content. Renders markdown with professional styling and outputs as PDF, HTML, or image. Use this instead of write_file for PDFs, reports, styled pages, or document screenshots. After creating, use send_file to deliver it to the user.',
|
|
307
|
+
schema: z.object({
|
|
308
|
+
content: z.string().describe('Markdown content for the document'),
|
|
309
|
+
title: z.string().optional().describe('Document title (shown in header and used for default filename)'),
|
|
310
|
+
filename: z.string().optional().describe('Output filename (defaults to title-based name with appropriate extension)'),
|
|
311
|
+
format: z.enum(['pdf', 'html', 'png', 'jpg']).optional().describe('Output format. "pdf" (default) for print-ready documents, "html" for web pages, "png"/"jpg" for images.'),
|
|
312
|
+
}),
|
|
313
|
+
},
|
|
314
|
+
),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
// create_spreadsheet: JSON data → xlsx or csv
|
|
318
|
+
tools.push(
|
|
319
|
+
tool(
|
|
320
|
+
async ({ data, headers, sheetName, filename, format }) => {
|
|
321
|
+
try {
|
|
322
|
+
const fmt = format || 'xlsx'
|
|
323
|
+
let rows: Record<string, unknown>[]
|
|
324
|
+
try {
|
|
325
|
+
rows = JSON.parse(data)
|
|
326
|
+
if (!Array.isArray(rows)) return 'Error: data must be a JSON array of objects'
|
|
327
|
+
} catch {
|
|
328
|
+
return 'Error: data is not valid JSON. Pass a JSON array of objects, e.g. [{"name":"Alice","age":30}]'
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!rows.length) return 'Error: data array is empty'
|
|
332
|
+
|
|
333
|
+
// Resolve column headers: explicit headers, or keys from first row
|
|
334
|
+
const cols = headers?.length
|
|
335
|
+
? headers
|
|
336
|
+
: Object.keys(rows[0] && typeof rows[0] === 'object' ? rows[0] : {})
|
|
337
|
+
if (!cols.length) return 'Error: could not determine column headers. Pass headers or use objects with keys.'
|
|
338
|
+
|
|
339
|
+
const defaultBase = (sheetName || 'spreadsheet').replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
340
|
+
|
|
341
|
+
if (fmt === 'csv') {
|
|
342
|
+
const escapeCsv = (val: unknown): string => {
|
|
343
|
+
const s = val == null ? '' : String(val)
|
|
344
|
+
return s.includes(',') || s.includes('"') || s.includes('\n')
|
|
345
|
+
? `"${s.replace(/"/g, '""')}"`
|
|
346
|
+
: s
|
|
347
|
+
}
|
|
348
|
+
const lines = [cols.map(escapeCsv).join(',')]
|
|
349
|
+
for (const row of rows) {
|
|
350
|
+
const r = Array.isArray(row) ? row : cols.map((c) => (row as Record<string, unknown>)[c])
|
|
351
|
+
lines.push(r.map(escapeCsv).join(','))
|
|
352
|
+
}
|
|
353
|
+
const outName = filename || `${defaultBase}.csv`
|
|
354
|
+
const resolved = safePath(bctx.cwd, outName)
|
|
355
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
356
|
+
fs.writeFileSync(resolved, lines.join('\n'), 'utf-8')
|
|
357
|
+
return `CSV created: ${outName} (${rows.length} rows, ${cols.length} columns)`
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// xlsx via exceljs
|
|
361
|
+
const ExcelJS = await import('exceljs')
|
|
362
|
+
const workbook = new ExcelJS.default.Workbook()
|
|
363
|
+
const sheet = workbook.addWorksheet(sheetName || 'Sheet1')
|
|
364
|
+
|
|
365
|
+
sheet.columns = cols.map((c) => ({ header: c, key: c, width: Math.max(12, c.length + 4) }))
|
|
366
|
+
// Style header row
|
|
367
|
+
sheet.getRow(1).font = { bold: true }
|
|
368
|
+
sheet.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF3F4F6' } }
|
|
369
|
+
|
|
370
|
+
for (const row of rows) {
|
|
371
|
+
if (Array.isArray(row)) {
|
|
372
|
+
const obj: Record<string, unknown> = {}
|
|
373
|
+
cols.forEach((c, i) => { obj[c] = row[i] })
|
|
374
|
+
sheet.addRow(obj)
|
|
375
|
+
} else {
|
|
376
|
+
sheet.addRow(row)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const outName = filename || `${defaultBase}.xlsx`
|
|
381
|
+
const resolved = safePath(bctx.cwd, outName)
|
|
382
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
383
|
+
await workbook.xlsx.writeFile(resolved)
|
|
384
|
+
const size = fs.statSync(resolved).size
|
|
385
|
+
return `Excel spreadsheet created: ${outName} (${rows.length} rows, ${cols.length} columns, ${(size / 1024).toFixed(1)} KB)`
|
|
386
|
+
} catch (err: any) {
|
|
387
|
+
return `Error creating spreadsheet: ${err.message}`
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: 'create_spreadsheet',
|
|
392
|
+
description: 'Create an Excel (.xlsx) or CSV file from structured data. Pass data as a JSON array of objects. Use this for tables, reports, data exports, and any tabular data the user requests. After creating, use send_file to deliver it to the user.',
|
|
393
|
+
schema: z.object({
|
|
394
|
+
data: z.string().describe('JSON array of objects, e.g. [{"name":"Alice","score":95},{"name":"Bob","score":87}]'),
|
|
395
|
+
headers: z.array(z.string()).optional().describe('Column headers in display order. If omitted, keys from the first object are used.'),
|
|
396
|
+
sheetName: z.string().optional().describe('Worksheet name (default "Sheet1")'),
|
|
397
|
+
filename: z.string().optional().describe('Output filename (defaults to sheetName-based name with extension)'),
|
|
398
|
+
format: z.enum(['xlsx', 'csv']).optional().describe('Output format: "xlsx" (default) for Excel, "csv" for plain CSV.'),
|
|
399
|
+
}),
|
|
400
|
+
},
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
|
|
232
405
|
if (bctx.hasTool('edit_file')) {
|
|
233
406
|
tools.push(
|
|
234
407
|
tool(
|
|
@@ -15,6 +15,7 @@ import { buildSessionInfoTools } from './session-info'
|
|
|
15
15
|
import { buildConnectorTools } from './connector'
|
|
16
16
|
import { buildContextTools } from './context-mgmt'
|
|
17
17
|
import { buildSandboxTools } from './sandbox'
|
|
18
|
+
import { buildOpenClawNodeTools } from './openclaw-nodes'
|
|
18
19
|
|
|
19
20
|
export type { ToolContext, SessionToolsResult }
|
|
20
21
|
export { sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser }
|
|
@@ -95,6 +96,7 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
|
|
|
95
96
|
...buildConnectorTools(bctx),
|
|
96
97
|
...buildContextTools(bctx),
|
|
97
98
|
...buildSandboxTools(bctx),
|
|
99
|
+
...buildOpenClawNodeTools(bctx),
|
|
98
100
|
)
|
|
99
101
|
|
|
100
102
|
// ---------------------------------------------------------------------------
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
3
|
import fs from 'fs'
|
|
4
|
-
import
|
|
4
|
+
import { genId } from '@/lib/id'
|
|
5
5
|
import { getMemoryDb, getMemoryLookupLimits, storeMemoryImageAsset } from '../memory-db'
|
|
6
6
|
import { loadSettings } from '../storage'
|
|
7
7
|
import type { ToolBuildContext } from './context'
|
|
@@ -81,7 +81,7 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
|
|
|
81
81
|
return `Error: image file not found: ${imagePath}`
|
|
82
82
|
}
|
|
83
83
|
try {
|
|
84
|
-
storedImage = await storeMemoryImageAsset(imagePath,
|
|
84
|
+
storedImage = await storeMemoryImageAsset(imagePath, genId(6))
|
|
85
85
|
} catch {
|
|
86
86
|
return `Error: failed to process image at ${imagePath}`
|
|
87
87
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import type { ToolBuildContext } from './context'
|
|
4
|
+
|
|
5
|
+
export function buildOpenClawNodeTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
6
|
+
if (!bctx.hasTool('openclaw_nodes')) return []
|
|
7
|
+
|
|
8
|
+
const tools: StructuredToolInterface[] = []
|
|
9
|
+
|
|
10
|
+
tools.push(
|
|
11
|
+
tool(
|
|
12
|
+
async () => {
|
|
13
|
+
try {
|
|
14
|
+
const { listRunningConnectors } = await import('../connectors/manager')
|
|
15
|
+
const openclawConnectors = listRunningConnectors('openclaw')
|
|
16
|
+
if (!openclawConnectors.length) {
|
|
17
|
+
return JSON.stringify({ error: 'No running OpenClaw connector found.' })
|
|
18
|
+
}
|
|
19
|
+
const { getRunningInstance } = await import('../connectors/manager')
|
|
20
|
+
const inst = getRunningInstance(openclawConnectors[0].id)
|
|
21
|
+
if (!inst) return JSON.stringify({ error: 'OpenClaw connector instance not accessible.' })
|
|
22
|
+
|
|
23
|
+
// Proxy through RPC — use sendMessage as a workaround to invoke RPC
|
|
24
|
+
// We need direct RPC access, so check if the instance exposes it
|
|
25
|
+
// For now, return a helpful message about the integration
|
|
26
|
+
return JSON.stringify({
|
|
27
|
+
status: 'openclaw_nodes_list requires nodes.list RPC support on the gateway',
|
|
28
|
+
connectorId: openclawConnectors[0].id,
|
|
29
|
+
note: 'This feature requires the OpenClaw gateway to support nodes.* RPCs.',
|
|
30
|
+
})
|
|
31
|
+
} catch (err: any) {
|
|
32
|
+
return JSON.stringify({ error: err.message })
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'openclaw_nodes_list',
|
|
37
|
+
description: 'List connected nodes/IoT devices through the OpenClaw gateway. Requires a running OpenClaw connector with nodes.* RPC support.',
|
|
38
|
+
schema: z.object({}),
|
|
39
|
+
},
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
tools.push(
|
|
44
|
+
tool(
|
|
45
|
+
async ({ nodeId, action, params }) => {
|
|
46
|
+
try {
|
|
47
|
+
const { listRunningConnectors, getRunningInstance } = await import('../connectors/manager')
|
|
48
|
+
const openclawConnectors = listRunningConnectors('openclaw')
|
|
49
|
+
if (!openclawConnectors.length) {
|
|
50
|
+
return JSON.stringify({ error: 'No running OpenClaw connector found.' })
|
|
51
|
+
}
|
|
52
|
+
const inst = getRunningInstance(openclawConnectors[0].id)
|
|
53
|
+
if (!inst) return JSON.stringify({ error: 'OpenClaw connector instance not accessible.' })
|
|
54
|
+
|
|
55
|
+
return JSON.stringify({
|
|
56
|
+
status: 'openclaw_node_invoke requires nodes.invoke RPC support on the gateway',
|
|
57
|
+
nodeId,
|
|
58
|
+
action,
|
|
59
|
+
params: params || null,
|
|
60
|
+
connectorId: openclawConnectors[0].id,
|
|
61
|
+
})
|
|
62
|
+
} catch (err: any) {
|
|
63
|
+
return JSON.stringify({ error: err.message })
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'openclaw_node_invoke',
|
|
68
|
+
description: 'Invoke an action on a connected node/IoT device through the OpenClaw gateway.',
|
|
69
|
+
schema: z.object({
|
|
70
|
+
nodeId: z.string().describe('Target node ID'),
|
|
71
|
+
action: z.string().describe('Action to invoke on the node'),
|
|
72
|
+
params: z.record(z.string(), z.unknown()).optional().describe('Optional parameters for the action'),
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
tools.push(
|
|
79
|
+
tool(
|
|
80
|
+
async ({ nodeId, message }) => {
|
|
81
|
+
try {
|
|
82
|
+
const { listRunningConnectors, getRunningInstance } = await import('../connectors/manager')
|
|
83
|
+
const openclawConnectors = listRunningConnectors('openclaw')
|
|
84
|
+
if (!openclawConnectors.length) {
|
|
85
|
+
return JSON.stringify({ error: 'No running OpenClaw connector found.' })
|
|
86
|
+
}
|
|
87
|
+
const inst = getRunningInstance(openclawConnectors[0].id)
|
|
88
|
+
if (!inst) return JSON.stringify({ error: 'OpenClaw connector instance not accessible.' })
|
|
89
|
+
|
|
90
|
+
return JSON.stringify({
|
|
91
|
+
status: 'openclaw_node_notify requires nodes.notify RPC support on the gateway',
|
|
92
|
+
nodeId,
|
|
93
|
+
message,
|
|
94
|
+
connectorId: openclawConnectors[0].id,
|
|
95
|
+
})
|
|
96
|
+
} catch (err: any) {
|
|
97
|
+
return JSON.stringify({ error: err.message })
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'openclaw_node_notify',
|
|
102
|
+
description: 'Send a notification to a connected node/IoT device through the OpenClaw gateway.',
|
|
103
|
+
schema: z.object({
|
|
104
|
+
nodeId: z.string().describe('Target node ID'),
|
|
105
|
+
message: z.string().describe('Notification message'),
|
|
106
|
+
}),
|
|
107
|
+
},
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return tools
|
|
112
|
+
}
|