@swarmclawai/swarmclaw 0.7.7 → 0.8.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 +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -5,6 +5,7 @@ import fs from 'fs'
|
|
|
5
5
|
import { loadConnectors, loadSettings, UPLOAD_DIR } from '../storage'
|
|
6
6
|
import { genId } from '@/lib/id'
|
|
7
7
|
import { synthesizeElevenLabsMp3 } from '../elevenlabs'
|
|
8
|
+
import { isAudioMime, mimeFromPath } from '../connectors/media'
|
|
8
9
|
import type { ToolBuildContext } from './context'
|
|
9
10
|
import type { Plugin, PluginHooks } from '@/types'
|
|
10
11
|
import { getPluginManager } from '../plugins'
|
|
@@ -17,6 +18,78 @@ const recentConnectorActionCache = new Map<string, { at: number; result: string
|
|
|
17
18
|
const connectorTurnSendBudget = new Map<string, { count: number; at: number; lastResult?: string }>()
|
|
18
19
|
const autonomousOutreachBudget = new Map<string, { at: number; result?: string }>()
|
|
19
20
|
|
|
21
|
+
export const CONNECTOR_MESSAGE_TOOL_ACTIONS = [
|
|
22
|
+
'list_running',
|
|
23
|
+
'list_targets',
|
|
24
|
+
'start',
|
|
25
|
+
'stop',
|
|
26
|
+
'send',
|
|
27
|
+
'send_voice_note',
|
|
28
|
+
'schedule_followup',
|
|
29
|
+
'react',
|
|
30
|
+
'edit',
|
|
31
|
+
'delete',
|
|
32
|
+
'pin',
|
|
33
|
+
'message_react',
|
|
34
|
+
'message_edit',
|
|
35
|
+
'message_delete',
|
|
36
|
+
'message_pin',
|
|
37
|
+
] as const
|
|
38
|
+
|
|
39
|
+
export const CONNECTOR_MESSAGE_TOOL_PARAMETERS = {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
action: { type: 'string', enum: [...CONNECTOR_MESSAGE_TOOL_ACTIONS] },
|
|
43
|
+
connectorId: { type: 'string' },
|
|
44
|
+
connector: { type: 'string' },
|
|
45
|
+
connector_id: { type: 'string' },
|
|
46
|
+
runningConnectorId: { type: 'string' },
|
|
47
|
+
id: { type: 'string' },
|
|
48
|
+
platform: { type: 'string' },
|
|
49
|
+
to: { type: 'string' },
|
|
50
|
+
channel: { type: 'string' },
|
|
51
|
+
channelId: { type: 'string' },
|
|
52
|
+
recipientId: { type: 'string' },
|
|
53
|
+
phoneNumber: { type: 'string' },
|
|
54
|
+
configuredTarget: { type: 'string' },
|
|
55
|
+
target: { type: 'string' },
|
|
56
|
+
recipient: { type: 'string' },
|
|
57
|
+
path: { type: 'string' },
|
|
58
|
+
targets: { type: 'string' },
|
|
59
|
+
message: { type: 'string' },
|
|
60
|
+
text: { type: 'string' },
|
|
61
|
+
content: { type: 'string' },
|
|
62
|
+
body: { type: 'string' },
|
|
63
|
+
messageId: { type: 'string' },
|
|
64
|
+
targetMessage: { type: 'string', enum: ['last_inbound', 'last_outbound'] },
|
|
65
|
+
emoji: { type: 'string' },
|
|
66
|
+
voiceText: { type: 'string' },
|
|
67
|
+
voiceId: { type: 'string' },
|
|
68
|
+
imageUrl: { type: 'string' },
|
|
69
|
+
fileUrl: { type: 'string' },
|
|
70
|
+
mediaPath: { type: 'string' },
|
|
71
|
+
mimeType: { type: 'string' },
|
|
72
|
+
fileName: { type: 'string' },
|
|
73
|
+
caption: { type: 'string' },
|
|
74
|
+
replyToMessageId: { type: 'string' },
|
|
75
|
+
threadId: { type: 'string' },
|
|
76
|
+
delaySec: { type: 'number' },
|
|
77
|
+
followUpMessage: { type: 'string' },
|
|
78
|
+
followupMessage: { type: 'string' },
|
|
79
|
+
followUpDelaySec: { type: 'number' },
|
|
80
|
+
dedupeKey: { type: 'string' },
|
|
81
|
+
approved: { type: 'boolean' },
|
|
82
|
+
ptt: { type: 'boolean' },
|
|
83
|
+
},
|
|
84
|
+
} as const
|
|
85
|
+
|
|
86
|
+
const LEGACY_CONNECTOR_ACTION_ALIASES: Record<string, string> = {
|
|
87
|
+
message_react: 'react',
|
|
88
|
+
message_edit: 'edit',
|
|
89
|
+
message_delete: 'delete',
|
|
90
|
+
message_pin: 'pin',
|
|
91
|
+
}
|
|
92
|
+
|
|
20
93
|
function pruneOldConnectorToolState(now: number): void {
|
|
21
94
|
for (const [key, entry] of recentConnectorActionCache.entries()) {
|
|
22
95
|
if (now - entry.at > CONNECTOR_ACTION_DEDUPE_TTL_MS) recentConnectorActionCache.delete(key)
|
|
@@ -109,6 +182,130 @@ function normalizeDedupedReplayResult(raw: string, fallback: { connectorId: stri
|
|
|
109
182
|
}
|
|
110
183
|
}
|
|
111
184
|
|
|
185
|
+
export function normalizeConnectorActionName(action: string): string {
|
|
186
|
+
const normalized = String(action || '').trim()
|
|
187
|
+
return LEGACY_CONNECTOR_ACTION_ALIASES[normalized] || normalized
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function inferConnectorActionName(input: Record<string, unknown>): string | null {
|
|
191
|
+
const explicit = typeof input.action === 'string' ? input.action.trim() : ''
|
|
192
|
+
if (explicit) return explicit
|
|
193
|
+
if (typeof input.voiceText === 'string' && input.voiceText.trim()) return 'send_voice_note'
|
|
194
|
+
if (
|
|
195
|
+
typeof input.followUpMessage === 'string'
|
|
196
|
+
|| typeof input.followupMessage === 'string'
|
|
197
|
+
|| typeof input.followUpDelaySec === 'number'
|
|
198
|
+
|| typeof input.delaySec === 'number'
|
|
199
|
+
) return 'schedule_followup'
|
|
200
|
+
if (
|
|
201
|
+
typeof input.message === 'string'
|
|
202
|
+
|| typeof input.text === 'string'
|
|
203
|
+
|| typeof input.content === 'string'
|
|
204
|
+
|| typeof input.body === 'string'
|
|
205
|
+
|| typeof input.mediaPath === 'string'
|
|
206
|
+
|| typeof input.imageUrl === 'string'
|
|
207
|
+
|| typeof input.fileUrl === 'string'
|
|
208
|
+
) return 'send'
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function pickConnectorString(value: unknown): string | null {
|
|
213
|
+
if (typeof value === 'string') {
|
|
214
|
+
const trimmed = value.trim()
|
|
215
|
+
return trimmed || null
|
|
216
|
+
}
|
|
217
|
+
if (Array.isArray(value)) {
|
|
218
|
+
for (const entry of value) {
|
|
219
|
+
const picked = pickConnectorString(entry)
|
|
220
|
+
if (picked) return picked
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function resolveRunningConnectorId(
|
|
227
|
+
running: Array<{ id?: string; name?: string }>,
|
|
228
|
+
value: unknown,
|
|
229
|
+
): string | null {
|
|
230
|
+
const candidate = pickConnectorString(value)
|
|
231
|
+
if (!candidate) return null
|
|
232
|
+
const matched = running.find((connector) => (
|
|
233
|
+
String(connector.id || '').trim() === candidate
|
|
234
|
+
|| String(connector.name || '').trim() === candidate
|
|
235
|
+
))
|
|
236
|
+
return matched ? String(matched.id || '').trim() || null : null
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function normalizeConnectorActionInputAliases(
|
|
240
|
+
input: Record<string, unknown>,
|
|
241
|
+
running: Array<{ id?: string; name?: string }> = [],
|
|
242
|
+
): Record<string, unknown> {
|
|
243
|
+
const normalized = { ...input }
|
|
244
|
+
const actionName = normalizeConnectorActionName(inferConnectorActionName(normalized) || String(normalized.action || ''))
|
|
245
|
+
const messageActionUsesRawId = actionName === 'react'
|
|
246
|
+
|| actionName === 'edit'
|
|
247
|
+
|| actionName === 'delete'
|
|
248
|
+
|| actionName === 'pin'
|
|
249
|
+
const messageAlias = pickConnectorString(
|
|
250
|
+
normalized.message
|
|
251
|
+
?? normalized.text
|
|
252
|
+
?? normalized.content
|
|
253
|
+
?? normalized.body,
|
|
254
|
+
)
|
|
255
|
+
if (!pickConnectorString(normalized.message) && messageAlias) {
|
|
256
|
+
normalized.message = messageAlias
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const followUpAlias = pickConnectorString(
|
|
260
|
+
normalized.followUpMessage
|
|
261
|
+
?? normalized.followupMessage,
|
|
262
|
+
)
|
|
263
|
+
if (!pickConnectorString(normalized.followUpMessage) && followUpAlias) {
|
|
264
|
+
normalized.followUpMessage = followUpAlias
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const rawId = pickConnectorString(normalized.id)
|
|
268
|
+
const explicitConnectorId = pickConnectorString(
|
|
269
|
+
normalized.connectorId
|
|
270
|
+
?? normalized.runningConnectorId
|
|
271
|
+
?? normalized.connector
|
|
272
|
+
?? normalized.connector_id,
|
|
273
|
+
)
|
|
274
|
+
const aliasConnectorId = explicitConnectorId
|
|
275
|
+
? resolveRunningConnectorId(running, explicitConnectorId) || explicitConnectorId
|
|
276
|
+
: resolveRunningConnectorId(running, normalized.channel) || resolveRunningConnectorId(running, rawId)
|
|
277
|
+
|
|
278
|
+
if (!pickConnectorString(normalized.connectorId) && aliasConnectorId) {
|
|
279
|
+
normalized.connectorId = aliasConnectorId
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const rawIdIsConnector = !!(rawId && resolveRunningConnectorId(running, rawId))
|
|
283
|
+
if (!pickConnectorString(normalized.messageId) && rawId && !rawIdIsConnector && messageActionUsesRawId) {
|
|
284
|
+
normalized.messageId = rawId
|
|
285
|
+
}
|
|
286
|
+
const targetAlias = pickConnectorString(
|
|
287
|
+
normalized.to
|
|
288
|
+
?? normalized.channelId
|
|
289
|
+
?? normalized.recipientId
|
|
290
|
+
?? normalized.phoneNumber
|
|
291
|
+
?? normalized.configuredTarget
|
|
292
|
+
?? normalized.target
|
|
293
|
+
?? normalized.recipient
|
|
294
|
+
?? normalized.path
|
|
295
|
+
?? normalized.targets,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if (!pickConnectorString(normalized.to)) {
|
|
299
|
+
if (targetAlias) {
|
|
300
|
+
normalized.to = targetAlias
|
|
301
|
+
} else if (rawId && !rawIdIsConnector && !messageActionUsesRawId) {
|
|
302
|
+
normalized.to = rawId
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return normalized
|
|
307
|
+
}
|
|
308
|
+
|
|
112
309
|
/** Resolve /api/uploads/filename URLs to actual disk paths */
|
|
113
310
|
function resolveUploadUrl(url: string | undefined): { mediaPath: string; mimeType?: string } | null {
|
|
114
311
|
if (!url) return null
|
|
@@ -140,13 +337,83 @@ function parseCsv(raw: string | undefined): string[] {
|
|
|
140
337
|
return raw.split(',').map((s) => s.trim()).filter(Boolean)
|
|
141
338
|
}
|
|
142
339
|
|
|
340
|
+
function trimToString(value: unknown): string {
|
|
341
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function resolveSessionConnectorTargets(
|
|
345
|
+
session: {
|
|
346
|
+
connectorContext?: Record<string, unknown>
|
|
347
|
+
messages?: Array<Record<string, unknown>>
|
|
348
|
+
} | null | undefined,
|
|
349
|
+
connectorId: string,
|
|
350
|
+
): Array<{ channelId: string; senderId?: string; senderName?: string }> {
|
|
351
|
+
const targets: Array<{ channelId: string; senderId?: string; senderName?: string }> = []
|
|
352
|
+
const seen = new Set<string>()
|
|
353
|
+
const pushTarget = (target: { channelId: string; senderId?: string; senderName?: string } | null) => {
|
|
354
|
+
if (!target?.channelId || seen.has(target.channelId)) return
|
|
355
|
+
seen.add(target.channelId)
|
|
356
|
+
targets.push(target)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const context = session?.connectorContext
|
|
360
|
+
if (trimToString(context?.connectorId) === connectorId) {
|
|
361
|
+
const channelId = trimToString(context?.channelId)
|
|
362
|
+
pushTarget(channelId
|
|
363
|
+
? {
|
|
364
|
+
channelId,
|
|
365
|
+
senderId: trimToString(context?.senderId) || undefined,
|
|
366
|
+
senderName: trimToString(context?.senderName) || undefined,
|
|
367
|
+
}
|
|
368
|
+
: null)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const messages = Array.isArray(session?.messages) ? session.messages : []
|
|
372
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
373
|
+
const source = messages[i]?.source as Record<string, unknown> | undefined
|
|
374
|
+
if (!source || trimToString(source.connectorId) !== connectorId) continue
|
|
375
|
+
const channelId = trimToString(source.channelId)
|
|
376
|
+
if (!channelId) continue
|
|
377
|
+
pushTarget({
|
|
378
|
+
channelId,
|
|
379
|
+
senderId: trimToString(source.senderId) || undefined,
|
|
380
|
+
senderName: trimToString(source.senderName) || undefined,
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return targets
|
|
385
|
+
}
|
|
386
|
+
|
|
143
387
|
function pickChannelTarget(params: {
|
|
144
388
|
connector: { config?: Record<string, string> }
|
|
389
|
+
connectorId: string
|
|
145
390
|
to?: string
|
|
146
391
|
recentChannelId: string | null
|
|
392
|
+
currentSession?: {
|
|
393
|
+
connectorContext?: Record<string, unknown>
|
|
394
|
+
messages?: Array<Record<string, unknown>>
|
|
395
|
+
} | null
|
|
147
396
|
}): { channelId: string; error?: string } {
|
|
148
397
|
let channelId = params.to?.trim() || ''
|
|
149
398
|
const connector = params.connector
|
|
399
|
+
const sessionTargets = resolveSessionConnectorTargets(params.currentSession, params.connectorId)
|
|
400
|
+
|
|
401
|
+
if (!channelId && sessionTargets.length === 1) {
|
|
402
|
+
channelId = sessionTargets[0].channelId
|
|
403
|
+
}
|
|
404
|
+
if (!channelId && sessionTargets.length > 1) {
|
|
405
|
+
const choices = sessionTargets.map((target) => (
|
|
406
|
+
target.senderName
|
|
407
|
+
? `${target.senderName} (${target.channelId})`
|
|
408
|
+
: target.senderId
|
|
409
|
+
? `${target.senderId} (${target.channelId})`
|
|
410
|
+
: target.channelId
|
|
411
|
+
))
|
|
412
|
+
return {
|
|
413
|
+
channelId: '',
|
|
414
|
+
error: `Error: this chat currently references multiple connector recipients for this connector: ${JSON.stringify(choices)}. Re-call with the "to" parameter so the message goes to the right person.`,
|
|
415
|
+
}
|
|
416
|
+
}
|
|
150
417
|
|
|
151
418
|
if (!channelId) {
|
|
152
419
|
const outbound = connector.config?.outboundJid?.trim()
|
|
@@ -189,7 +456,7 @@ function pickChannelTarget(params: {
|
|
|
189
456
|
return { channelId }
|
|
190
457
|
}
|
|
191
458
|
|
|
192
|
-
function resolveConnectorMediaInput(params: {
|
|
459
|
+
export function resolveConnectorMediaInput(params: {
|
|
193
460
|
cwd: string
|
|
194
461
|
mediaPath?: string
|
|
195
462
|
imageUrl?: string
|
|
@@ -199,6 +466,23 @@ function resolveConnectorMediaInput(params: {
|
|
|
199
466
|
let resolvedImageUrl = params.imageUrl?.trim() || undefined
|
|
200
467
|
let resolvedFileUrl = params.fileUrl?.trim() || undefined
|
|
201
468
|
|
|
469
|
+
// Be forgiving when the model passes a served upload URL or remote URL in mediaPath.
|
|
470
|
+
if (resolvedMediaPath?.startsWith('/api/uploads/')) {
|
|
471
|
+
const fromUpload = resolveUploadUrl(resolvedMediaPath)
|
|
472
|
+
if (fromUpload) {
|
|
473
|
+
resolvedMediaPath = fromUpload.mediaPath
|
|
474
|
+
} else {
|
|
475
|
+
return { error: `Error: File not found: ${resolvedMediaPath}` }
|
|
476
|
+
}
|
|
477
|
+
} else if (resolvedMediaPath && /^https?:\/\//i.test(resolvedMediaPath)) {
|
|
478
|
+
if (/\.(png|jpe?g|webp|gif|svg)(?:[?#].*)?$/i.test(resolvedMediaPath)) {
|
|
479
|
+
resolvedImageUrl = resolvedMediaPath
|
|
480
|
+
} else {
|
|
481
|
+
resolvedFileUrl = resolvedMediaPath
|
|
482
|
+
}
|
|
483
|
+
resolvedMediaPath = undefined
|
|
484
|
+
}
|
|
485
|
+
|
|
202
486
|
if (resolvedMediaPath && !path.isAbsolute(resolvedMediaPath) && !resolvedMediaPath.startsWith('/api/uploads/')) {
|
|
203
487
|
const candidatePaths = [
|
|
204
488
|
path.resolve(params.cwd, resolvedMediaPath),
|
|
@@ -273,33 +557,10 @@ interface ConnectorActionContext {
|
|
|
273
557
|
}
|
|
274
558
|
|
|
275
559
|
async function executeConnectorAction(input: ConnectorActionInput, bctx: ConnectorActionContext) {
|
|
276
|
-
const
|
|
277
|
-
const {
|
|
278
|
-
action,
|
|
279
|
-
connectorId,
|
|
280
|
-
platform,
|
|
281
|
-
to,
|
|
282
|
-
message,
|
|
283
|
-
voiceText,
|
|
284
|
-
voiceId,
|
|
285
|
-
imageUrl,
|
|
286
|
-
fileUrl,
|
|
287
|
-
mediaPath,
|
|
288
|
-
mimeType,
|
|
289
|
-
fileName,
|
|
290
|
-
caption,
|
|
291
|
-
messageId,
|
|
292
|
-
targetMessage,
|
|
293
|
-
emoji,
|
|
294
|
-
replyToMessageId,
|
|
295
|
-
threadId,
|
|
296
|
-
dedupeKey,
|
|
297
|
-
approved,
|
|
298
|
-
ptt,
|
|
299
|
-
} = normalized as ConnectorActionInput
|
|
560
|
+
const baseNormalized = normalizeToolInputArgs((input ?? {}) as Record<string, unknown>)
|
|
300
561
|
|
|
301
562
|
try {
|
|
302
|
-
const
|
|
563
|
+
const tentativePlatform = pickConnectorString(baseNormalized.platform)
|
|
303
564
|
const {
|
|
304
565
|
listRunningConnectors,
|
|
305
566
|
sendConnectorMessage,
|
|
@@ -307,7 +568,34 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
|
|
|
307
568
|
scheduleConnectorFollowUp,
|
|
308
569
|
performConnectorMessageAction,
|
|
309
570
|
} = await import('../connectors/manager')
|
|
310
|
-
const running = listRunningConnectors(
|
|
571
|
+
const running = listRunningConnectors(tentativePlatform || undefined)
|
|
572
|
+
const normalized = normalizeConnectorActionInputAliases(baseNormalized, running)
|
|
573
|
+
const inferredAction = inferConnectorActionName(normalized)
|
|
574
|
+
const {
|
|
575
|
+
action,
|
|
576
|
+
connectorId,
|
|
577
|
+
platform,
|
|
578
|
+
to,
|
|
579
|
+
message,
|
|
580
|
+
voiceText,
|
|
581
|
+
voiceId,
|
|
582
|
+
imageUrl,
|
|
583
|
+
fileUrl,
|
|
584
|
+
mediaPath,
|
|
585
|
+
mimeType,
|
|
586
|
+
fileName,
|
|
587
|
+
caption,
|
|
588
|
+
messageId,
|
|
589
|
+
targetMessage,
|
|
590
|
+
emoji,
|
|
591
|
+
replyToMessageId,
|
|
592
|
+
threadId,
|
|
593
|
+
dedupeKey,
|
|
594
|
+
approved,
|
|
595
|
+
ptt,
|
|
596
|
+
} = normalized as ConnectorActionInput
|
|
597
|
+
const actionName = normalizeConnectorActionName(String(inferredAction || action || ''))
|
|
598
|
+
if (!actionName) return 'Error: action is required.'
|
|
311
599
|
|
|
312
600
|
if (actionName === 'list_running' || actionName === 'list_targets') {
|
|
313
601
|
return JSON.stringify(running)
|
|
@@ -374,8 +662,10 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
|
|
|
374
662
|
|
|
375
663
|
const target = pickChannelTarget({
|
|
376
664
|
connector,
|
|
665
|
+
connectorId: selected.id,
|
|
377
666
|
to,
|
|
378
667
|
recentChannelId: getConnectorRecentChannelId(selected.id),
|
|
668
|
+
currentSession,
|
|
379
669
|
})
|
|
380
670
|
if (target.error) return target.error
|
|
381
671
|
|
|
@@ -399,22 +689,61 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
|
|
|
399
689
|
}
|
|
400
690
|
|
|
401
691
|
if (actionName === 'send_voice_note') {
|
|
692
|
+
const media = resolveConnectorMediaInput({ cwd: bctx.cwd, mediaPath, imageUrl, fileUrl })
|
|
693
|
+
if (media.error) return media.error
|
|
694
|
+
if (media.imageUrl || media.fileUrl) {
|
|
695
|
+
return 'Error: send_voice_note requires an audio mediaPath or voiceText. Remote image/file URLs are not valid voice-note inputs.'
|
|
696
|
+
}
|
|
402
697
|
const ttsText = (voiceText || message || '').trim()
|
|
403
|
-
if (!ttsText) return 'Error: voiceText or
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
698
|
+
if (!media.mediaPath && !ttsText) return 'Error: voiceText, message, or an audio mediaPath is required.'
|
|
699
|
+
const voiceActionKey = buildConnectorActionKey([
|
|
700
|
+
sessionId,
|
|
701
|
+
actionName,
|
|
702
|
+
selected.id,
|
|
703
|
+
channelId,
|
|
704
|
+
media.mediaPath || '',
|
|
705
|
+
ttsText,
|
|
706
|
+
voiceId?.trim() || '',
|
|
707
|
+
fileName?.trim() || '',
|
|
708
|
+
caption?.trim() || '',
|
|
709
|
+
ptt ?? true,
|
|
710
|
+
])
|
|
711
|
+
const cachedVoice = recentConnectorActionCache.get(voiceActionKey)
|
|
712
|
+
if (cachedVoice && now - cachedVoice.at <= CONNECTOR_ACTION_DEDUPE_TTL_MS) {
|
|
713
|
+
return cachedVoice.result
|
|
714
|
+
}
|
|
715
|
+
let voicePath = media.mediaPath
|
|
716
|
+
let outboundMimeType = mimeType?.trim() || undefined
|
|
717
|
+
if (voicePath) {
|
|
718
|
+
outboundMimeType = outboundMimeType || mimeFromPath(voicePath)
|
|
719
|
+
if (!isAudioMime(outboundMimeType)) {
|
|
720
|
+
return `Error: send_voice_note mediaPath must point to an audio file. Resolved MIME type was "${outboundMimeType}".`
|
|
721
|
+
}
|
|
722
|
+
} else {
|
|
723
|
+
const audioBuffer = await synthesizeElevenLabsMp3({ text: ttsText, voiceId: voiceId?.trim() || undefined })
|
|
724
|
+
const voiceFileName = `${Date.now()}-${genId()}-voicenote.mp3`
|
|
725
|
+
voicePath = path.join(UPLOAD_DIR, voiceFileName)
|
|
726
|
+
fs.writeFileSync(voicePath, audioBuffer)
|
|
727
|
+
outboundMimeType = 'audio/mpeg'
|
|
728
|
+
}
|
|
408
729
|
|
|
409
730
|
const sent = await sendConnectorMessage({
|
|
410
|
-
connectorId: selected.id, channelId, text: '', mediaPath: voicePath, mimeType:
|
|
731
|
+
connectorId: selected.id, channelId, text: '', mediaPath: voicePath, mimeType: outboundMimeType,
|
|
411
732
|
fileName: fileName?.trim() || 'voicenote.mp3', caption: caption?.trim() || undefined, ptt: ptt ?? true,
|
|
412
733
|
sessionId,
|
|
413
734
|
replyToMessageId: replyToMessageId?.trim() || undefined,
|
|
414
735
|
threadId: threadId?.trim() || undefined,
|
|
415
736
|
})
|
|
416
|
-
const result = JSON.stringify({
|
|
737
|
+
const result = JSON.stringify({
|
|
738
|
+
status: 'voice_sent',
|
|
739
|
+
connectorId: sent.connectorId,
|
|
740
|
+
platform: sent.platform,
|
|
741
|
+
to: sent.channelId,
|
|
742
|
+
messageId: sent.messageId || null,
|
|
743
|
+
voiceFile: voicePath,
|
|
744
|
+
})
|
|
417
745
|
connectorTurnSendBudget.set(turnKey, { count: (existingBudget?.count || 0) + 1, at: now, lastResult: result })
|
|
746
|
+
recentConnectorActionCache.set(voiceActionKey, { at: now, result })
|
|
418
747
|
return result
|
|
419
748
|
}
|
|
420
749
|
|
|
@@ -486,8 +815,10 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
|
|
|
486
815
|
const { selected } = resolved
|
|
487
816
|
const target = pickChannelTarget({
|
|
488
817
|
connector: resolved.connector,
|
|
818
|
+
connectorId: selected.id,
|
|
489
819
|
to,
|
|
490
820
|
recentChannelId: getConnectorRecentChannelId(selected.id),
|
|
821
|
+
currentSession,
|
|
491
822
|
})
|
|
492
823
|
if (target.error) return target.error
|
|
493
824
|
const result = await performConnectorMessageAction({
|
|
@@ -529,26 +860,7 @@ const ConnectorPlugin: Plugin = {
|
|
|
529
860
|
{
|
|
530
861
|
name: 'connector_message_tool',
|
|
531
862
|
description: 'Send and manage outbound messages across chat platforms.',
|
|
532
|
-
parameters:
|
|
533
|
-
type: 'object',
|
|
534
|
-
properties: {
|
|
535
|
-
action: { type: 'string', enum: ['list_running', 'start', 'stop', 'send', 'send_voice_note', 'schedule_followup', 'react', 'edit', 'delete', 'pin'] },
|
|
536
|
-
connectorId: { type: 'string' },
|
|
537
|
-
platform: { type: 'string' },
|
|
538
|
-
to: { type: 'string' },
|
|
539
|
-
message: { type: 'string' },
|
|
540
|
-
messageId: { type: 'string' },
|
|
541
|
-
targetMessage: { type: 'string', enum: ['last_inbound', 'last_outbound'] },
|
|
542
|
-
emoji: { type: 'string' },
|
|
543
|
-
replyToMessageId: { type: 'string' },
|
|
544
|
-
threadId: { type: 'string' },
|
|
545
|
-
delaySec: { type: 'number' },
|
|
546
|
-
followUpMessage: { type: 'string' },
|
|
547
|
-
followUpDelaySec: { type: 'number' },
|
|
548
|
-
dedupeKey: { type: 'string' },
|
|
549
|
-
},
|
|
550
|
-
required: ['action']
|
|
551
|
-
},
|
|
863
|
+
parameters: CONNECTOR_MESSAGE_TOOL_PARAMETERS,
|
|
552
864
|
execute: async (args, context) => executeConnectorAction(args as ConnectorActionInput, { ...context.session, cwd: context.session.cwd || process.cwd() })
|
|
553
865
|
}
|
|
554
866
|
]
|
|
@@ -9,6 +9,11 @@ export interface ToolContext {
|
|
|
9
9
|
platformAssignScope?: 'self' | 'all'
|
|
10
10
|
mcpServerIds?: string[]
|
|
11
11
|
mcpDisabledTools?: string[]
|
|
12
|
+
projectId?: string | null
|
|
13
|
+
projectRoot?: string | null
|
|
14
|
+
projectName?: string | null
|
|
15
|
+
projectDescription?: string | null
|
|
16
|
+
memoryScopeMode?: 'auto' | 'all' | 'global' | 'agent' | 'session' | 'project' | null
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
export interface SessionToolsResult {
|
|
@@ -43,15 +48,24 @@ function normalizeWorkspaceAlias(cwd: string, filePath: string): string {
|
|
|
43
48
|
return trimmed
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Safe absolute paths that agents are allowed to write to outside the workspace.
|
|
53
|
+
* Kept minimal to prevent accidental writes to sensitive system locations.
|
|
54
|
+
*/
|
|
55
|
+
const ALLOWED_ABSOLUTE_PREFIXES = ['/tmp/', '/var/tmp/']
|
|
56
|
+
|
|
46
57
|
export function safePath(cwd: string, filePath: string): string {
|
|
47
58
|
const path = require('path')
|
|
48
59
|
const normalized = normalizeWorkspaceAlias(cwd, filePath)
|
|
49
60
|
const resolvedRoot = path.resolve(cwd)
|
|
50
61
|
const resolved = path.resolve(resolvedRoot, normalized)
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
// Allow workspace-relative paths
|
|
63
|
+
if (resolved.startsWith(resolvedRoot)) return resolved
|
|
64
|
+
// Allow explicitly safe absolute paths (e.g., /tmp/)
|
|
65
|
+
if (path.isAbsolute(normalized) && ALLOWED_ABSOLUTE_PREFIXES.some((p: string) => resolved.startsWith(p))) {
|
|
66
|
+
return resolved
|
|
53
67
|
}
|
|
54
|
-
|
|
68
|
+
throw new Error('Path traversal not allowed')
|
|
55
69
|
}
|
|
56
70
|
|
|
57
71
|
export function truncate(text: string, max: number): string {
|