@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.
Files changed (209) hide show
  1. package/README.md +21 -4
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +13 -0
  4. package/package.json +3 -1
  5. package/src/app/api/agents/[id]/route.ts +39 -22
  6. package/src/app/api/agents/[id]/thread/route.ts +2 -2
  7. package/src/app/api/agents/route.ts +3 -2
  8. package/src/app/api/agents/trash/route.ts +44 -0
  9. package/src/app/api/clawhub/install/route.ts +2 -2
  10. package/src/app/api/connectors/[id]/route.ts +17 -7
  11. package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
  12. package/src/app/api/connectors/route.ts +6 -3
  13. package/src/app/api/credentials/[id]/route.ts +2 -1
  14. package/src/app/api/credentials/route.ts +2 -2
  15. package/src/app/api/documents/route.ts +2 -2
  16. package/src/app/api/files/serve/route.ts +8 -0
  17. package/src/app/api/knowledge/[id]/route.ts +5 -4
  18. package/src/app/api/knowledge/upload/route.ts +2 -2
  19. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  20. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  21. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  22. package/src/app/api/mcp-servers/route.ts +2 -2
  23. package/src/app/api/memory/[id]/route.ts +9 -8
  24. package/src/app/api/memory/route.ts +2 -2
  25. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  26. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  27. package/src/app/api/openclaw/approvals/route.ts +46 -0
  28. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  29. package/src/app/api/openclaw/cron/route.ts +52 -0
  30. package/src/app/api/openclaw/directory/route.ts +27 -0
  31. package/src/app/api/openclaw/discover/route.ts +62 -0
  32. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  33. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  34. package/src/app/api/openclaw/gateway/route.ts +72 -0
  35. package/src/app/api/openclaw/history/route.ts +109 -0
  36. package/src/app/api/openclaw/media/route.ts +53 -0
  37. package/src/app/api/openclaw/models/route.ts +12 -0
  38. package/src/app/api/openclaw/permissions/route.ts +39 -0
  39. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  40. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  41. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  42. package/src/app/api/openclaw/skills/route.ts +82 -0
  43. package/src/app/api/openclaw/sync/route.ts +31 -0
  44. package/src/app/api/orchestrator/run/route.ts +2 -2
  45. package/src/app/api/projects/[id]/route.ts +55 -0
  46. package/src/app/api/projects/route.ts +27 -0
  47. package/src/app/api/providers/[id]/models/route.ts +2 -1
  48. package/src/app/api/providers/[id]/route.ts +13 -15
  49. package/src/app/api/providers/route.ts +2 -2
  50. package/src/app/api/schedules/[id]/route.ts +16 -18
  51. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  52. package/src/app/api/schedules/route.ts +2 -2
  53. package/src/app/api/secrets/[id]/route.ts +16 -17
  54. package/src/app/api/secrets/route.ts +2 -2
  55. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  56. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  57. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  58. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  59. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  60. package/src/app/api/sessions/[id]/messages/route.ts +20 -2
  61. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  62. package/src/app/api/sessions/[id]/route.ts +14 -4
  63. package/src/app/api/sessions/route.ts +8 -4
  64. package/src/app/api/skills/[id]/route.ts +23 -21
  65. package/src/app/api/skills/import/route.ts +2 -2
  66. package/src/app/api/skills/route.ts +2 -2
  67. package/src/app/api/tasks/[id]/approve/route.ts +2 -1
  68. package/src/app/api/tasks/[id]/route.ts +6 -5
  69. package/src/app/api/tasks/route.ts +2 -2
  70. package/src/app/api/tts/stream/route.ts +48 -0
  71. package/src/app/api/upload/route.ts +2 -2
  72. package/src/app/api/uploads/[filename]/route.ts +4 -1
  73. package/src/app/api/webhooks/[id]/route.ts +29 -31
  74. package/src/app/api/webhooks/route.ts +2 -2
  75. package/src/app/globals.css +14 -0
  76. package/src/app/layout.tsx +5 -20
  77. package/src/app/page.tsx +3 -24
  78. package/src/cli/index.js +60 -0
  79. package/src/cli/index.ts +1 -1
  80. package/src/cli/spec.js +42 -0
  81. package/src/components/agents/agent-avatar.tsx +45 -0
  82. package/src/components/agents/agent-card.tsx +19 -5
  83. package/src/components/agents/agent-chat-list.tsx +31 -24
  84. package/src/components/agents/agent-files-editor.tsx +185 -0
  85. package/src/components/agents/agent-list.tsx +84 -3
  86. package/src/components/agents/agent-sheet.tsx +147 -14
  87. package/src/components/agents/cron-job-form.tsx +137 -0
  88. package/src/components/agents/exec-config-panel.tsx +147 -0
  89. package/src/components/agents/inspector-panel.tsx +310 -0
  90. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  91. package/src/components/agents/permission-preset-selector.tsx +79 -0
  92. package/src/components/agents/personality-builder.tsx +111 -0
  93. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  94. package/src/components/agents/skill-install-dialog.tsx +102 -0
  95. package/src/components/agents/trash-list.tsx +109 -0
  96. package/src/components/chat/chat-area.tsx +41 -6
  97. package/src/components/chat/chat-header.tsx +305 -29
  98. package/src/components/chat/chat-preview-panel.tsx +113 -0
  99. package/src/components/chat/exec-approval-card.tsx +89 -0
  100. package/src/components/chat/message-bubble.tsx +218 -36
  101. package/src/components/chat/message-list.tsx +135 -31
  102. package/src/components/chat/streaming-bubble.tsx +59 -10
  103. package/src/components/chat/suggestions-bar.tsx +74 -0
  104. package/src/components/chat/thinking-indicator.tsx +20 -6
  105. package/src/components/chat/tool-call-bubble.tsx +98 -19
  106. package/src/components/chat/tool-request-banner.tsx +20 -2
  107. package/src/components/chat/trace-block.tsx +103 -0
  108. package/src/components/chat/voice-overlay.tsx +80 -0
  109. package/src/components/connectors/connector-list.tsx +6 -2
  110. package/src/components/connectors/connector-sheet.tsx +31 -7
  111. package/src/components/layout/app-layout.tsx +47 -25
  112. package/src/components/projects/project-list.tsx +123 -0
  113. package/src/components/projects/project-sheet.tsx +135 -0
  114. package/src/components/schedules/schedule-list.tsx +3 -1
  115. package/src/components/sessions/new-session-sheet.tsx +6 -6
  116. package/src/components/sessions/session-card.tsx +1 -1
  117. package/src/components/sessions/session-list.tsx +7 -7
  118. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  119. package/src/components/shared/avatar.tsx +13 -2
  120. package/src/components/shared/connector-platform-icon.tsx +4 -0
  121. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  122. package/src/components/shared/settings/section-orchestrator.tsx +1 -2
  123. package/src/components/shared/settings/section-web-search.tsx +56 -0
  124. package/src/components/shared/settings/settings-page.tsx +74 -0
  125. package/src/components/skills/skill-list.tsx +2 -1
  126. package/src/components/tasks/task-board.tsx +1 -1
  127. package/src/components/tasks/task-list.tsx +5 -2
  128. package/src/components/tasks/task-sheet.tsx +12 -12
  129. package/src/hooks/use-continuous-speech.ts +181 -0
  130. package/src/hooks/use-openclaw-gateway.ts +63 -0
  131. package/src/hooks/use-view-router.ts +52 -0
  132. package/src/hooks/use-voice-conversation.ts +80 -0
  133. package/src/lib/id.ts +6 -0
  134. package/src/lib/notification-sounds.ts +58 -0
  135. package/src/lib/personality-parser.ts +97 -0
  136. package/src/lib/projects.ts +13 -0
  137. package/src/lib/provider-sets.ts +5 -0
  138. package/src/lib/providers/anthropic.ts +14 -1
  139. package/src/lib/providers/index.ts +6 -0
  140. package/src/lib/providers/ollama.ts +9 -1
  141. package/src/lib/providers/openai.ts +9 -1
  142. package/src/lib/providers/openclaw.ts +28 -2
  143. package/src/lib/runtime-loop.ts +2 -2
  144. package/src/lib/server/api-routes.test.ts +5 -6
  145. package/src/lib/server/build-llm.ts +17 -4
  146. package/src/lib/server/chat-execution.ts +82 -6
  147. package/src/lib/server/collection-helpers.ts +54 -0
  148. package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
  149. package/src/lib/server/connectors/bluebubbles.ts +360 -0
  150. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  151. package/src/lib/server/connectors/googlechat.ts +51 -8
  152. package/src/lib/server/connectors/manager.ts +424 -13
  153. package/src/lib/server/connectors/media.ts +2 -2
  154. package/src/lib/server/connectors/openclaw.ts +65 -0
  155. package/src/lib/server/connectors/pairing.test.ts +99 -0
  156. package/src/lib/server/connectors/pairing.ts +256 -0
  157. package/src/lib/server/connectors/signal.ts +1 -0
  158. package/src/lib/server/connectors/teams.ts +5 -5
  159. package/src/lib/server/connectors/types.ts +10 -0
  160. package/src/lib/server/daemon-state.ts +11 -0
  161. package/src/lib/server/execution-log.ts +3 -3
  162. package/src/lib/server/heartbeat-service.ts +1 -1
  163. package/src/lib/server/knowledge-db.test.ts +2 -33
  164. package/src/lib/server/main-agent-loop.ts +8 -9
  165. package/src/lib/server/main-session.ts +21 -0
  166. package/src/lib/server/memory-db.ts +6 -6
  167. package/src/lib/server/openclaw-approvals.ts +105 -0
  168. package/src/lib/server/openclaw-config-sync.ts +107 -0
  169. package/src/lib/server/openclaw-exec-config.ts +52 -0
  170. package/src/lib/server/openclaw-gateway.ts +291 -0
  171. package/src/lib/server/openclaw-history-merge.ts +36 -0
  172. package/src/lib/server/openclaw-models.ts +56 -0
  173. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  174. package/src/lib/server/openclaw-sync.ts +497 -0
  175. package/src/lib/server/orchestrator-lg.ts +30 -9
  176. package/src/lib/server/orchestrator.ts +4 -4
  177. package/src/lib/server/process-manager.ts +2 -2
  178. package/src/lib/server/queue.ts +24 -11
  179. package/src/lib/server/scheduler.ts +2 -2
  180. package/src/lib/server/session-mailbox.ts +2 -2
  181. package/src/lib/server/session-run-manager.ts +2 -2
  182. package/src/lib/server/session-tools/connector.ts +53 -6
  183. package/src/lib/server/session-tools/crud.ts +3 -3
  184. package/src/lib/server/session-tools/delegate.ts +22 -6
  185. package/src/lib/server/session-tools/file.ts +192 -19
  186. package/src/lib/server/session-tools/index.ts +4 -2
  187. package/src/lib/server/session-tools/memory.ts +2 -2
  188. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  189. package/src/lib/server/session-tools/sandbox.ts +33 -0
  190. package/src/lib/server/session-tools/search-providers.ts +277 -0
  191. package/src/lib/server/session-tools/session-info.ts +2 -2
  192. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  193. package/src/lib/server/session-tools/shell.ts +1 -1
  194. package/src/lib/server/session-tools/web.ts +53 -72
  195. package/src/lib/server/storage.ts +74 -11
  196. package/src/lib/server/stream-agent-chat.ts +53 -4
  197. package/src/lib/server/suggestions.ts +20 -0
  198. package/src/lib/server/task-result.test.ts +44 -0
  199. package/src/lib/server/task-result.ts +14 -0
  200. package/src/lib/server/ws-hub.ts +14 -0
  201. package/src/lib/tool-definitions.ts +5 -3
  202. package/src/lib/tts-stream.ts +130 -0
  203. package/src/lib/view-routes.ts +28 -0
  204. package/src/proxy.ts +3 -0
  205. package/src/stores/use-app-store.ts +80 -1
  206. package/src/stores/use-approval-store.ts +78 -0
  207. package/src/stores/use-chat-store.ts +162 -6
  208. package/src/types/index.ts +154 -3
  209. package/tsconfig.json +13 -4
@@ -1,4 +1,4 @@
1
- import crypto from 'crypto'
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 = crypto.randomBytes(8).toString('hex')
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()
@@ -1,4 +1,4 @@
1
- import crypto from 'crypto'
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?.name === '__main__'
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(runSession, task.result || fallbackText || null)
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(runSession, task.result || fallbackText || null)
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: crypto.randomBytes(4).toString('hex'),
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: crypto.randomBytes(4).toString('hex'),
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: crypto.randomBytes(4).toString('hex'),
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(runSessions[sessionId], result || null)
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: crypto.randomBytes(4).toString('hex'),
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: crypto.randomBytes(4).toString('hex'),
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: crypto.randomBytes(4).toString('hex'),
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 crypto from 'crypto'
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 = crypto.randomBytes(4).toString('hex')
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 crypto from 'crypto'
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: crypto.randomBytes(6).toString('hex'),
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 crypto from 'crypto'
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 = crypto.randomBytes(8).toString('hex')
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
- return `Error: no target recipient configured. Provide "to", or set connector config "outboundJid"/"allowedJids".`
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: any) {
98
- return `Error: ${err.message || String(err)}`
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 through running connectors (for example WhatsApp status updates). Supports listing running connectors/targets and sending text plus optional media (URLs or local file paths).',
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 crypto from 'crypto'
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 = crypto.randomBytes(4).toString('hex')
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 = crypto.randomBytes(6).toString('hex')
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 crypto from 'crypto'
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. Use for tasks that need deep code understanding, multi-file refactoring, or running tests. The task runs in the session working directory.',
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. Use for deep coding/refactor tasks and shell-driven implementation work.',
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. Use for deep coding/refactor tasks and shell-driven implementation work.',
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 = crypto.randomBytes(4).toString('hex')
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: crypto.randomBytes(4).toString('hex'),
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
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, '&lt;')
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: any) {
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
  {