@swarmclawai/swarmclaw 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -4
- package/bin/server-cmd.js +28 -19
- package/next.config.ts +13 -0
- package/package.json +3 -1
- package/src/app/api/agents/[id]/route.ts +39 -22
- package/src/app/api/agents/[id]/thread/route.ts +2 -2
- package/src/app/api/agents/route.ts +3 -2
- package/src/app/api/agents/trash/route.ts +44 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +17 -7
- package/src/app/api/connectors/[id]/webhook/route.ts +103 -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/agent-files/route.ts +57 -0
- package/src/app/api/openclaw/approvals/route.ts +46 -0
- package/src/app/api/openclaw/config-sync/route.ts +33 -0
- package/src/app/api/openclaw/cron/route.ts +52 -0
- package/src/app/api/openclaw/directory/route.ts +27 -0
- package/src/app/api/openclaw/discover/route.ts +62 -0
- package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
- package/src/app/api/openclaw/exec-config/route.ts +41 -0
- package/src/app/api/openclaw/gateway/route.ts +72 -0
- package/src/app/api/openclaw/history/route.ts +109 -0
- package/src/app/api/openclaw/media/route.ts +53 -0
- package/src/app/api/openclaw/models/route.ts +12 -0
- package/src/app/api/openclaw/permissions/route.ts +39 -0
- package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
- package/src/app/api/openclaw/skills/install/route.ts +32 -0
- package/src/app/api/openclaw/skills/remove/route.ts +24 -0
- package/src/app/api/openclaw/skills/route.ts +82 -0
- package/src/app/api/openclaw/sync/route.ts +31 -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]/edit-resend/route.ts +22 -0
- package/src/app/api/sessions/[id]/fork/route.ts +44 -0
- package/src/app/api/sessions/[id]/messages/route.ts +20 -2
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +14 -4
- package/src/app/api/sessions/route.ts +8 -4
- 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/globals.css +14 -0
- package/src/app/layout.tsx +5 -20
- package/src/app/page.tsx +3 -24
- package/src/cli/index.js +60 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +45 -0
- package/src/components/agents/agent-card.tsx +19 -5
- package/src/components/agents/agent-chat-list.tsx +31 -24
- package/src/components/agents/agent-files-editor.tsx +185 -0
- package/src/components/agents/agent-list.tsx +84 -3
- package/src/components/agents/agent-sheet.tsx +147 -14
- package/src/components/agents/cron-job-form.tsx +137 -0
- package/src/components/agents/exec-config-panel.tsx +147 -0
- package/src/components/agents/inspector-panel.tsx +310 -0
- package/src/components/agents/openclaw-skills-panel.tsx +230 -0
- package/src/components/agents/permission-preset-selector.tsx +79 -0
- package/src/components/agents/personality-builder.tsx +111 -0
- package/src/components/agents/sandbox-env-panel.tsx +72 -0
- package/src/components/agents/skill-install-dialog.tsx +102 -0
- package/src/components/agents/trash-list.tsx +109 -0
- package/src/components/chat/chat-area.tsx +41 -6
- package/src/components/chat/chat-header.tsx +305 -29
- package/src/components/chat/chat-preview-panel.tsx +113 -0
- package/src/components/chat/exec-approval-card.tsx +89 -0
- package/src/components/chat/message-bubble.tsx +218 -36
- package/src/components/chat/message-list.tsx +135 -31
- package/src/components/chat/streaming-bubble.tsx +59 -10
- package/src/components/chat/suggestions-bar.tsx +74 -0
- package/src/components/chat/thinking-indicator.tsx +20 -6
- package/src/components/chat/tool-call-bubble.tsx +98 -19
- package/src/components/chat/tool-request-banner.tsx +20 -2
- package/src/components/chat/trace-block.tsx +103 -0
- 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 +123 -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/settings/gateway-connection-panel.tsx +278 -0
- package/src/components/shared/avatar.tsx +13 -2
- 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 +74 -0
- package/src/components/skills/skill-list.tsx +2 -1
- package/src/components/tasks/task-board.tsx +1 -1
- package/src/components/tasks/task-list.tsx +5 -2
- package/src/components/tasks/task-sheet.tsx +12 -12
- package/src/hooks/use-continuous-speech.ts +181 -0
- package/src/hooks/use-openclaw-gateway.ts +63 -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/notification-sounds.ts +58 -0
- package/src/lib/personality-parser.ts +97 -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 +28 -2
- package/src/lib/runtime-loop.ts +2 -2
- 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 +82 -6
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
- package/src/lib/server/connectors/bluebubbles.ts +360 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +51 -8
- package/src/lib/server/connectors/manager.ts +424 -13
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +65 -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/daemon-state.ts +11 -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 +8 -9
- package/src/lib/server/main-session.ts +21 -0
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-config-sync.ts +107 -0
- package/src/lib/server/openclaw-exec-config.ts +52 -0
- package/src/lib/server/openclaw-gateway.ts +291 -0
- package/src/lib/server/openclaw-history-merge.ts +36 -0
- package/src/lib/server/openclaw-models.ts +56 -0
- package/src/lib/server/openclaw-permission-presets.ts +64 -0
- package/src/lib/server/openclaw-sync.ts +497 -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 +24 -11
- 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 +53 -6
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +22 -6
- package/src/lib/server/session-tools/file.ts +192 -19
- package/src/lib/server/session-tools/index.ts +4 -2
- 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 +277 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/web.ts +53 -72
- package/src/lib/server/storage.ts +74 -11
- package/src/lib/server/stream-agent-chat.ts +53 -4
- package/src/lib/server/suggestions.ts +20 -0
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/server/ws-hub.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 +80 -1
- package/src/stores/use-approval-store.ts +78 -0
- package/src/stores/use-chat-store.ts +162 -6
- package/src/types/index.ts +154 -3
- package/tsconfig.json +13 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
2
|
import { spawn, type ChildProcessWithoutNullStreams } from 'child_process'
|
|
3
3
|
|
|
4
4
|
const MAX_LOG_CHARS = 200_000
|
|
@@ -99,7 +99,7 @@ function getShellCommand(command: string): { shell: string; args: string[] } {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
export async function startManagedProcess(opts: StartProcessOptions): Promise<StartProcessResult> {
|
|
102
|
-
const id =
|
|
102
|
+
const id = genId(8)
|
|
103
103
|
const timeoutMs = Math.max(1000, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
104
104
|
const yieldMs = Math.max(250, opts.yieldMs ?? DEFAULT_BACKGROUND_YIELD_MS)
|
|
105
105
|
const startedAt = now()
|
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'
|
|
@@ -9,6 +9,7 @@ import { pushMainLoopEventToMainSessions } from './main-agent-loop'
|
|
|
9
9
|
import { executeSessionChatTurn } from './chat-execution'
|
|
10
10
|
import { extractTaskResult, formatResultBody } from './task-result'
|
|
11
11
|
import { getCheckpointSaver } from './langgraph-checkpoint'
|
|
12
|
+
import { isProtectedMainSession } from './main-session'
|
|
12
13
|
import type { Agent, BoardTask, Message } from '@/types'
|
|
13
14
|
|
|
14
15
|
let processing = false
|
|
@@ -82,7 +83,7 @@ function pushQueueUnique(queue: string[], id: string): void {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
function isMainSession(session: SessionLike | null | undefined): boolean {
|
|
85
|
-
return session
|
|
86
|
+
return isProtectedMainSession(session)
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
function resolveTaskOwnerUser(task: ScheduleTaskMeta, sessions: Record<string, SessionLike>): string | null {
|
|
@@ -155,7 +156,11 @@ function notifyMainChatScheduleResult(task: BoardTask): void {
|
|
|
155
156
|
const fallbackText = runSession ? latestAssistantText(runSession) : ''
|
|
156
157
|
|
|
157
158
|
// Zod-validated structured extraction: one pass to get summary + all artifacts
|
|
158
|
-
const taskResult = extractTaskResult(
|
|
159
|
+
const taskResult = extractTaskResult(
|
|
160
|
+
runSession,
|
|
161
|
+
task.result || fallbackText || null,
|
|
162
|
+
{ sinceTime: typeof task.startedAt === 'number' ? task.startedAt : null },
|
|
163
|
+
)
|
|
159
164
|
const resultBody = formatResultBody(taskResult)
|
|
160
165
|
|
|
161
166
|
const statusLabel = task.status === 'completed' ? 'completed' : 'failed'
|
|
@@ -224,7 +229,11 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
|
|
|
224
229
|
const runSessionId = typeof task.sessionId === 'string' ? task.sessionId : ''
|
|
225
230
|
const runSession = runSessionId ? sessions[runSessionId] : null
|
|
226
231
|
const fallbackText = runSession ? latestAssistantText(runSession) : ''
|
|
227
|
-
const taskResult = extractTaskResult(
|
|
232
|
+
const taskResult = extractTaskResult(
|
|
233
|
+
runSession,
|
|
234
|
+
task.result || fallbackText || null,
|
|
235
|
+
{ sinceTime: typeof task.startedAt === 'number' ? task.startedAt : null },
|
|
236
|
+
)
|
|
228
237
|
const resultBody = formatResultBody(taskResult)
|
|
229
238
|
|
|
230
239
|
const statusLabel = task.status === 'completed' ? 'completed' : 'failed'
|
|
@@ -374,7 +383,7 @@ export function validateCompletedTasksQueue() {
|
|
|
374
383
|
task.updatedAt = now
|
|
375
384
|
if (!task.comments) task.comments = []
|
|
376
385
|
task.comments.push({
|
|
377
|
-
id:
|
|
386
|
+
id: genId(),
|
|
378
387
|
author: 'System',
|
|
379
388
|
text: `Task auto-failed completed-queue validation.\n\n${validation.reasons.map((r) => `- ${r}`).join('\n')}`,
|
|
380
389
|
createdAt: now,
|
|
@@ -413,7 +422,7 @@ function scheduleRetryOrDeadLetter(task: BoardTask, reason: string): 'retry' | '
|
|
|
413
422
|
task.error = `Retry scheduled after failure: ${reason}`.slice(0, 500)
|
|
414
423
|
if (!task.comments) task.comments = []
|
|
415
424
|
task.comments.push({
|
|
416
|
-
id:
|
|
425
|
+
id: genId(),
|
|
417
426
|
author: 'System',
|
|
418
427
|
text: `Attempt ${task.attempts}/${task.maxAttempts} failed. Retrying in ${delaySec}s.\n\nReason: ${reason}`,
|
|
419
428
|
createdAt: now,
|
|
@@ -428,7 +437,7 @@ function scheduleRetryOrDeadLetter(task: BoardTask, reason: string): 'retry' | '
|
|
|
428
437
|
task.error = `Dead-lettered after ${task.attempts}/${task.maxAttempts} attempts: ${reason}`.slice(0, 500)
|
|
429
438
|
if (!task.comments) task.comments = []
|
|
430
439
|
task.comments.push({
|
|
431
|
-
id:
|
|
440
|
+
id: genId(),
|
|
432
441
|
author: 'System',
|
|
433
442
|
text: `Task moved to dead-letter after ${task.attempts}/${task.maxAttempts} attempts.\n\nReason: ${reason}`,
|
|
434
443
|
createdAt: now,
|
|
@@ -610,7 +619,11 @@ export async function processNext() {
|
|
|
610
619
|
applyTaskPolicyDefaults(t2[taskId])
|
|
611
620
|
// Structured extraction: Zod-validated result with typed artifacts
|
|
612
621
|
const runSessions = loadSessions()
|
|
613
|
-
const taskResult = extractTaskResult(
|
|
622
|
+
const taskResult = extractTaskResult(
|
|
623
|
+
runSessions[sessionId],
|
|
624
|
+
result || null,
|
|
625
|
+
{ sinceTime: typeof t2[taskId].startedAt === 'number' ? t2[taskId].startedAt : null },
|
|
626
|
+
)
|
|
614
627
|
const enrichedResult = formatResultBody(taskResult)
|
|
615
628
|
t2[taskId].result = enrichedResult.slice(0, 4000) || null
|
|
616
629
|
t2[taskId].updatedAt = Date.now()
|
|
@@ -636,7 +649,7 @@ export async function processNext() {
|
|
|
636
649
|
updatedAt: now,
|
|
637
650
|
}
|
|
638
651
|
t2[taskId].comments!.push({
|
|
639
|
-
id:
|
|
652
|
+
id: genId(),
|
|
640
653
|
author: agent.name,
|
|
641
654
|
agentId: agent.id,
|
|
642
655
|
text: `Task completed.\n\n${result?.slice(0, 1000) || 'No summary provided.'}`,
|
|
@@ -647,7 +660,7 @@ export async function processNext() {
|
|
|
647
660
|
const retryState = scheduleRetryOrDeadLetter(t2[taskId], failureReason)
|
|
648
661
|
t2[taskId].completedAt = retryState === 'dead_lettered' ? null : t2[taskId].completedAt
|
|
649
662
|
t2[taskId].comments!.push({
|
|
650
|
-
id:
|
|
663
|
+
id: genId(),
|
|
651
664
|
author: agent.name,
|
|
652
665
|
agentId: agent.id,
|
|
653
666
|
text: `Task failed validation and was not marked completed.\n\n${validation.reasons.map((r) => `- ${r}`).join('\n')}`,
|
|
@@ -739,7 +752,7 @@ export async function processNext() {
|
|
|
739
752
|
const isRepeatError = lastComment?.agentId === agent.id && lastComment?.text.startsWith('Task failed')
|
|
740
753
|
if (!isRepeatError) {
|
|
741
754
|
t2[taskId].comments!.push({
|
|
742
|
-
id:
|
|
755
|
+
id: genId(),
|
|
743
756
|
author: agent.name,
|
|
744
757
|
agentId: agent.id,
|
|
745
758
|
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,18 +101,57 @@ 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: unknown) {
|
|
139
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
96
143
|
return 'Unknown action. Use list_running, list_targets, or send.'
|
|
97
|
-
} catch (err:
|
|
98
|
-
return `Error: ${err.message
|
|
144
|
+
} catch (err: unknown) {
|
|
145
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
99
146
|
}
|
|
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'
|
|
@@ -199,6 +199,21 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
199
199
|
return
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
// If resume failed because the session no longer exists, clear the stale ID
|
|
203
|
+
// and return a targeted error so the agent retries without resume
|
|
204
|
+
if (resumeIdToUse && /No conversation found/i.test(stdout + stderr)) {
|
|
205
|
+
persistDelegateResumeId('claudeCode', null)
|
|
206
|
+
log.warn('session-tools', 'delegate_to_claude_code stale resume ID cleared', {
|
|
207
|
+
sessionId: ctx?.sessionId || null,
|
|
208
|
+
staleResumeId: resumeIdToUse,
|
|
209
|
+
})
|
|
210
|
+
finish(
|
|
211
|
+
`Error: The previous Claude Code session (${resumeIdToUse}) has expired and was cleared. ` +
|
|
212
|
+
'Retry the task with resume=false to start a fresh session.',
|
|
213
|
+
)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
202
217
|
const successText = assistantText.trim() || stdout.trim() || stderr.trim()
|
|
203
218
|
if (code === 0 && successText) {
|
|
204
219
|
const out = discoveredSessionId
|
|
@@ -230,7 +245,7 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
230
245
|
},
|
|
231
246
|
{
|
|
232
247
|
name: 'delegate_to_claude_code',
|
|
233
|
-
description: 'Delegate a complex task to Claude Code CLI.
|
|
248
|
+
description: 'Delegate a complex multi-file coding task to Claude Code CLI. ONLY for deep code understanding, multi-file refactoring, or large code generation. NEVER use this to run servers, dev servers, install dependencies, or execute commands — use execute_command for those (this tool\'s session ends and kills any running processes).',
|
|
234
249
|
schema: z.object({
|
|
235
250
|
task: z.string().describe('Detailed description of the task for Claude Code'),
|
|
236
251
|
resume: z.boolean().optional().describe('If true, try to resume the last saved Claude delegation session for this SwarmClaw session'),
|
|
@@ -442,7 +457,7 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
442
457
|
},
|
|
443
458
|
{
|
|
444
459
|
name: 'delegate_to_codex_cli',
|
|
445
|
-
description: 'Delegate a complex task to Codex CLI.
|
|
460
|
+
description: 'Delegate a complex multi-file coding task to Codex CLI. ONLY for deep code understanding, multi-file refactoring, or large code generation. NEVER use this to run servers, dev servers, install dependencies, or execute commands — use execute_command for those (this tool\'s session ends and kills any running processes).',
|
|
446
461
|
schema: z.object({
|
|
447
462
|
task: z.string().describe('Detailed description of the task for Codex CLI'),
|
|
448
463
|
resume: z.boolean().optional().describe('If true, try to resume the last saved Codex delegation thread for this SwarmClaw session'),
|
|
@@ -607,7 +622,7 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
607
622
|
},
|
|
608
623
|
{
|
|
609
624
|
name: 'delegate_to_opencode_cli',
|
|
610
|
-
description: 'Delegate a complex task to OpenCode CLI.
|
|
625
|
+
description: 'Delegate a complex multi-file coding task to OpenCode CLI. ONLY for deep code understanding, multi-file refactoring, or large code generation. NEVER use this to run servers, dev servers, install dependencies, or execute commands — use execute_command for those (this tool\'s session ends and kills any running processes).',
|
|
611
626
|
schema: z.object({
|
|
612
627
|
task: z.string().describe('Detailed description of the task for OpenCode CLI'),
|
|
613
628
|
resume: z.boolean().optional().describe('If true, try to resume the last saved OpenCode delegation session for this SwarmClaw session'),
|
|
@@ -640,7 +655,7 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
640
655
|
}
|
|
641
656
|
if (!target) return `Error: Agent "${targetAgentId}" not found. Use the agent directory in your system prompt to find valid agent IDs.`
|
|
642
657
|
|
|
643
|
-
const taskId =
|
|
658
|
+
const taskId = genId()
|
|
644
659
|
const now = Date.now()
|
|
645
660
|
const newTask = {
|
|
646
661
|
id: taskId,
|
|
@@ -648,12 +663,13 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
648
663
|
description: taskDesc || taskPrompt,
|
|
649
664
|
status: 'todo',
|
|
650
665
|
agentId: resolvedId,
|
|
666
|
+
cwd,
|
|
651
667
|
sourceType: 'delegation' as const,
|
|
652
668
|
delegatedByAgentId: ctx.agentId!,
|
|
653
669
|
createdAt: now,
|
|
654
670
|
updatedAt: now,
|
|
655
671
|
comments: [{
|
|
656
|
-
id:
|
|
672
|
+
id: genId(),
|
|
657
673
|
author: agents[ctx.agentId!]?.name || 'Agent',
|
|
658
674
|
agentId: ctx.agentId!,
|
|
659
675
|
text: `Delegated from ${agents[ctx.agentId!]?.name || ctx.agentId}`,
|
|
@@ -26,8 +26,8 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
26
26
|
const resolved = safePath(bctx.cwd, filePath)
|
|
27
27
|
const content = fs.readFileSync(resolved, 'utf-8')
|
|
28
28
|
return truncate(content, MAX_FILE)
|
|
29
|
-
} catch (err:
|
|
30
|
-
return `Error reading file: ${err.message}`
|
|
29
|
+
} catch (err: unknown) {
|
|
30
|
+
return `Error reading file: ${err instanceof Error ? err.message : String(err)}`
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
{
|
|
@@ -44,22 +44,28 @@ 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
|
-
} catch (err:
|
|
54
|
-
return `Error writing file: ${err.message}`
|
|
58
|
+
} catch (err: unknown) {
|
|
59
|
+
return `Error writing file: ${err instanceof Error ? err.message : String(err)}`
|
|
55
60
|
}
|
|
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
|
),
|
|
@@ -74,8 +80,8 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
74
80
|
const resolved = safePath(bctx.cwd, dirPath || '.')
|
|
75
81
|
const tree = listDirRecursive(resolved, 0, 3)
|
|
76
82
|
return tree.length ? tree.join('\n') : '(empty directory)'
|
|
77
|
-
} catch (err:
|
|
78
|
-
return `Error listing files: ${err.message}`
|
|
83
|
+
} catch (err: unknown) {
|
|
84
|
+
return `Error listing files: ${err instanceof Error ? err.message : String(err)}`
|
|
79
85
|
}
|
|
80
86
|
},
|
|
81
87
|
{
|
|
@@ -103,8 +109,8 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
103
109
|
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
|
104
110
|
fs.copyFileSync(source, destination)
|
|
105
111
|
return `File copied: ${sourcePath} -> ${destinationPath}`
|
|
106
|
-
} catch (err:
|
|
107
|
-
return `Error copying file: ${err.message}`
|
|
112
|
+
} catch (err: unknown) {
|
|
113
|
+
return `Error copying file: ${err instanceof Error ? err.message : String(err)}`
|
|
108
114
|
}
|
|
109
115
|
},
|
|
110
116
|
{
|
|
@@ -135,8 +141,8 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
135
141
|
if (fs.existsSync(destination) && overwrite) fs.unlinkSync(destination)
|
|
136
142
|
fs.renameSync(source, destination)
|
|
137
143
|
return `File moved: ${sourcePath} -> ${destinationPath}`
|
|
138
|
-
} catch (err:
|
|
139
|
-
return `Error moving file: ${err.message}`
|
|
144
|
+
} catch (err: unknown) {
|
|
145
|
+
return `Error moving file: ${err instanceof Error ? err.message : String(err)}`
|
|
140
146
|
}
|
|
141
147
|
},
|
|
142
148
|
{
|
|
@@ -169,8 +175,8 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
169
175
|
}
|
|
170
176
|
fs.rmSync(resolved, { recursive: !!recursive, force: !!force })
|
|
171
177
|
return `Deleted: ${filePath}`
|
|
172
|
-
} catch (err:
|
|
173
|
-
return `Error deleting file: ${err.message}`
|
|
178
|
+
} catch (err: unknown) {
|
|
179
|
+
return `Error deleting file: ${err instanceof Error ? err.message : String(err)}`
|
|
174
180
|
}
|
|
175
181
|
},
|
|
176
182
|
{
|
|
@@ -214,8 +220,8 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
214
220
|
} else {
|
|
215
221
|
return `[Download ${basename}](/api/uploads/${filename})`
|
|
216
222
|
}
|
|
217
|
-
} catch (err:
|
|
218
|
-
return `Error sending file: ${err.message}`
|
|
223
|
+
} catch (err: unknown) {
|
|
224
|
+
return `Error sending file: ${err instanceof Error ? err.message : String(err)}`
|
|
219
225
|
}
|
|
220
226
|
},
|
|
221
227
|
{
|
|
@@ -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: unknown) {
|
|
301
|
+
return `Error creating document: ${err instanceof Error ? err.message : String(err)}`
|
|
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: unknown) {
|
|
387
|
+
return `Error creating spreadsheet: ${err instanceof Error ? err.message : String(err)}`
|
|
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(
|
|
@@ -243,8 +416,8 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
243
416
|
const updated = content.replace(oldText, newText)
|
|
244
417
|
fs.writeFileSync(resolved, updated, 'utf-8')
|
|
245
418
|
return `Successfully edited ${filePath}`
|
|
246
|
-
} catch (err:
|
|
247
|
-
return `Error editing file: ${err.message}`
|
|
419
|
+
} catch (err: unknown) {
|
|
420
|
+
return `Error editing file: ${err instanceof Error ? err.message : String(err)}`
|
|
248
421
|
}
|
|
249
422
|
},
|
|
250
423
|
{
|