@swarmclawai/swarmclaw 0.7.1 → 0.7.3

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 (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -0,0 +1,366 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import type { Plugin, PluginHooks } from '@/types'
4
+ import { getPluginManager } from '../plugins'
5
+ import { normalizeToolInputArgs } from './normalize-tool-args'
6
+ import type { ToolBuildContext } from './context'
7
+
8
+ type CalendarProvider = 'google' | 'outlook'
9
+
10
+ interface CalendarConfig {
11
+ provider: CalendarProvider
12
+ accessToken: string
13
+ calendarId: string
14
+ refreshToken: string
15
+ clientId: string
16
+ clientSecret: string
17
+ }
18
+
19
+ function getConfig(): CalendarConfig {
20
+ const ps = getPluginManager().getPluginSettings('calendar')
21
+ return {
22
+ provider: (ps.provider as CalendarProvider) || 'google',
23
+ accessToken: (ps.accessToken as string) || '',
24
+ calendarId: (ps.calendarId as string) || 'primary',
25
+ refreshToken: (ps.refreshToken as string) || '',
26
+ clientId: (ps.clientId as string) || '',
27
+ clientSecret: (ps.clientSecret as string) || '',
28
+ }
29
+ }
30
+
31
+ /** Try to refresh the Google OAuth access token using the refresh token. */
32
+ async function refreshGoogleToken(cfg: CalendarConfig): Promise<string | null> {
33
+ if (!cfg.refreshToken || !cfg.clientId || !cfg.clientSecret) return null
34
+ try {
35
+ const res = await fetch('https://oauth2.googleapis.com/token', {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
38
+ body: new URLSearchParams({
39
+ grant_type: 'refresh_token',
40
+ refresh_token: cfg.refreshToken,
41
+ client_id: cfg.clientId,
42
+ client_secret: cfg.clientSecret,
43
+ }),
44
+ signal: AbortSignal.timeout(10_000),
45
+ })
46
+ if (!res.ok) return null
47
+ const data = await res.json()
48
+ const newToken = data?.access_token as string | undefined
49
+ if (newToken) {
50
+ getPluginManager().setPluginSettings('calendar', { ...cfg, accessToken: newToken })
51
+ }
52
+ return newToken || null
53
+ } catch {
54
+ return null
55
+ }
56
+ }
57
+
58
+ async function googleRequest(method: string, urlPath: string, cfg: CalendarConfig, body?: unknown): Promise<{ ok: boolean; data?: unknown; error?: string }> {
59
+ let token = cfg.accessToken
60
+ const baseUrl = 'https://www.googleapis.com/calendar/v3'
61
+
62
+ const doFetch = async (t: string) => {
63
+ const init: RequestInit = {
64
+ method,
65
+ headers: { Authorization: `Bearer ${t}`, 'Content-Type': 'application/json' },
66
+ signal: AbortSignal.timeout(15_000),
67
+ }
68
+ if (body && method !== 'GET' && method !== 'DELETE') init.body = JSON.stringify(body)
69
+ return fetch(`${baseUrl}${urlPath}`, init)
70
+ }
71
+
72
+ let res = await doFetch(token)
73
+ if (res.status === 401) {
74
+ const refreshed = await refreshGoogleToken(cfg)
75
+ if (refreshed) {
76
+ token = refreshed
77
+ res = await doFetch(token)
78
+ }
79
+ }
80
+
81
+ if (!res.ok) {
82
+ const errText = await res.text().catch(() => '')
83
+ return { ok: false, error: `Google Calendar ${res.status}: ${errText.slice(0, 300)}` }
84
+ }
85
+ if (method === 'DELETE') return { ok: true }
86
+ const data = await res.json()
87
+ return { ok: true, data }
88
+ }
89
+
90
+ async function outlookRequest(method: string, urlPath: string, cfg: CalendarConfig, body?: unknown): Promise<{ ok: boolean; data?: unknown; error?: string }> {
91
+ const baseUrl = 'https://graph.microsoft.com/v1.0/me'
92
+ const init: RequestInit = {
93
+ method,
94
+ headers: { Authorization: `Bearer ${cfg.accessToken}`, 'Content-Type': 'application/json' },
95
+ signal: AbortSignal.timeout(15_000),
96
+ }
97
+ if (body && method !== 'GET' && method !== 'DELETE') init.body = JSON.stringify(body)
98
+ const res = await fetch(`${baseUrl}${urlPath}`, init)
99
+ if (!res.ok) {
100
+ const errText = await res.text().catch(() => '')
101
+ return { ok: false, error: `Outlook ${res.status}: ${errText.slice(0, 300)}` }
102
+ }
103
+ if (method === 'DELETE') return { ok: true }
104
+ const data = await res.json()
105
+ return { ok: true, data }
106
+ }
107
+
108
+ function formatEvent(e: Record<string, unknown>): Record<string, unknown> {
109
+ return {
110
+ id: e.id,
111
+ summary: e.summary ?? e.subject,
112
+ start: (e.start as Record<string, unknown>)?.dateTime ?? (e.start as Record<string, unknown>)?.date ?? e.start,
113
+ end: (e.end as Record<string, unknown>)?.dateTime ?? (e.end as Record<string, unknown>)?.date ?? e.end,
114
+ location: e.location ?? (e.location as unknown as Record<string, unknown>)?.displayName,
115
+ description: typeof e.description === 'string' ? e.description.slice(0, 200) : (e.body as Record<string, unknown>)?.content?.toString().slice(0, 200),
116
+ status: e.status ?? e.showAs,
117
+ htmlLink: e.htmlLink ?? e.webLink,
118
+ }
119
+ }
120
+
121
+ async function executeCalendar(args: Record<string, unknown>): Promise<string> {
122
+ const normalized = normalizeToolInputArgs(args)
123
+ const action = String(normalized.action || 'list')
124
+ const cfg = getConfig()
125
+
126
+ if (!cfg.accessToken) {
127
+ return 'Error: Calendar not configured. Ask the user to add their access token in Plugin Settings > Calendar.'
128
+ }
129
+
130
+ try {
131
+ switch (action) {
132
+ case 'list': {
133
+ const timeMin = String(normalized.timeMin || new Date().toISOString())
134
+ const timeMax = normalized.timeMax as string | undefined
135
+ const maxResults = Math.min(Number(normalized.maxResults) || 20, 50)
136
+
137
+ if (cfg.provider === 'outlook') {
138
+ const params = new URLSearchParams({
139
+ $top: String(maxResults),
140
+ $orderby: 'start/dateTime',
141
+ $filter: `start/dateTime ge '${timeMin}'${timeMax ? ` and end/dateTime le '${timeMax}'` : ''}`,
142
+ })
143
+ const r = await outlookRequest('GET', `/calendar/events?${params}`, cfg)
144
+ if (!r.ok) return `Error: ${r.error}`
145
+ const events = ((r.data as Record<string, unknown>)?.value as Record<string, unknown>[]) ?? []
146
+ return JSON.stringify(events.map(formatEvent))
147
+ }
148
+
149
+ const params = new URLSearchParams({
150
+ timeMin,
151
+ maxResults: String(maxResults),
152
+ singleEvents: 'true',
153
+ orderBy: 'startTime',
154
+ })
155
+ if (timeMax) params.set('timeMax', timeMax)
156
+ const r = await googleRequest('GET', `/calendars/${encodeURIComponent(cfg.calendarId)}/events?${params}`, cfg)
157
+ if (!r.ok) return `Error: ${r.error}`
158
+ const events = ((r.data as Record<string, unknown>)?.items as Record<string, unknown>[]) ?? []
159
+ return JSON.stringify(events.map(formatEvent))
160
+ }
161
+
162
+ case 'create': {
163
+ const summary = String(normalized.summary || normalized.title || '').trim()
164
+ if (!summary) return 'Error: "summary" (event title) is required.'
165
+ const start = String(normalized.start || '').trim()
166
+ const end = String(normalized.end || '').trim()
167
+ if (!start) return 'Error: "start" (ISO datetime) is required.'
168
+
169
+ const description = (normalized.description as string) || ''
170
+ const location = (normalized.location as string) || ''
171
+
172
+ if (cfg.provider === 'outlook') {
173
+ const body = {
174
+ subject: summary,
175
+ body: { contentType: 'text', content: description },
176
+ start: { dateTime: start, timeZone: 'UTC' },
177
+ end: { dateTime: end || new Date(new Date(start).getTime() + 3600_000).toISOString(), timeZone: 'UTC' },
178
+ location: { displayName: location },
179
+ }
180
+ const r = await outlookRequest('POST', '/calendar/events', cfg, body)
181
+ if (!r.ok) return `Error: ${r.error}`
182
+ return `Event created: ${JSON.stringify(formatEvent(r.data as Record<string, unknown>))}`
183
+ }
184
+
185
+ const body = {
186
+ summary,
187
+ description,
188
+ location,
189
+ start: { dateTime: start, timeZone: 'UTC' },
190
+ end: { dateTime: end || new Date(new Date(start).getTime() + 3600_000).toISOString(), timeZone: 'UTC' },
191
+ }
192
+ const r = await googleRequest('POST', `/calendars/${encodeURIComponent(cfg.calendarId)}/events`, cfg, body)
193
+ if (!r.ok) return `Error: ${r.error}`
194
+ return `Event created: ${JSON.stringify(formatEvent(r.data as Record<string, unknown>))}`
195
+ }
196
+
197
+ case 'update': {
198
+ const eventId = String(normalized.eventId || normalized.id || '').trim()
199
+ if (!eventId) return 'Error: "eventId" is required.'
200
+ const updates: Record<string, unknown> = {}
201
+ if (normalized.summary) updates.summary = String(normalized.summary)
202
+ if (normalized.description) updates.description = String(normalized.description)
203
+ if (normalized.location) updates.location = String(normalized.location)
204
+ if (normalized.start) updates.start = { dateTime: String(normalized.start), timeZone: 'UTC' }
205
+ if (normalized.end) updates.end = { dateTime: String(normalized.end), timeZone: 'UTC' }
206
+
207
+ if (cfg.provider === 'outlook') {
208
+ const outlookUpdates: Record<string, unknown> = {}
209
+ if (normalized.summary) outlookUpdates.subject = String(normalized.summary)
210
+ if (normalized.description) outlookUpdates.body = { contentType: 'text', content: String(normalized.description) }
211
+ if (normalized.location) outlookUpdates.location = { displayName: String(normalized.location) }
212
+ if (normalized.start) outlookUpdates.start = { dateTime: String(normalized.start), timeZone: 'UTC' }
213
+ if (normalized.end) outlookUpdates.end = { dateTime: String(normalized.end), timeZone: 'UTC' }
214
+ const r = await outlookRequest('PATCH', `/calendar/events/${eventId}`, cfg, outlookUpdates)
215
+ if (!r.ok) return `Error: ${r.error}`
216
+ return `Event updated: ${JSON.stringify(formatEvent(r.data as Record<string, unknown>))}`
217
+ }
218
+
219
+ const r = await googleRequest('PATCH', `/calendars/${encodeURIComponent(cfg.calendarId)}/events/${eventId}`, cfg, updates)
220
+ if (!r.ok) return `Error: ${r.error}`
221
+ return `Event updated: ${JSON.stringify(formatEvent(r.data as Record<string, unknown>))}`
222
+ }
223
+
224
+ case 'delete': {
225
+ const eventId = String(normalized.eventId || normalized.id || '').trim()
226
+ if (!eventId) return 'Error: "eventId" is required.'
227
+
228
+ if (cfg.provider === 'outlook') {
229
+ const r = await outlookRequest('DELETE', `/calendar/events/${eventId}`, cfg)
230
+ if (!r.ok) return `Error: ${r.error}`
231
+ return `Event ${eventId} deleted.`
232
+ }
233
+
234
+ const r = await googleRequest('DELETE', `/calendars/${encodeURIComponent(cfg.calendarId)}/events/${eventId}`, cfg)
235
+ if (!r.ok) return `Error: ${r.error}`
236
+ return `Event ${eventId} deleted.`
237
+ }
238
+
239
+ case 'status': {
240
+ return JSON.stringify({
241
+ configured: true,
242
+ provider: cfg.provider,
243
+ calendarId: cfg.calendarId,
244
+ hasRefreshToken: !!cfg.refreshToken,
245
+ })
246
+ }
247
+
248
+ default:
249
+ return `Error: Unknown action "${action}". Use: list, create, update, delete, status.`
250
+ }
251
+ } catch (err: unknown) {
252
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
253
+ }
254
+ }
255
+
256
+ const CalendarPlugin: Plugin = {
257
+ name: 'Calendar',
258
+ enabledByDefault: false,
259
+ description: 'Manage Google Calendar or Outlook calendar events — list, create, update, delete.',
260
+ hooks: {
261
+ getCapabilityDescription: () =>
262
+ 'I can manage calendar events using `calendar`: list upcoming events, create new ones, update or delete existing events. Supports Google Calendar and Outlook.',
263
+ } as PluginHooks,
264
+ tools: [
265
+ {
266
+ name: 'calendar',
267
+ description: 'Manage calendar events. Actions: list (upcoming events), create (new event), update (modify event), delete (remove event), status (check config).',
268
+ parameters: {
269
+ type: 'object',
270
+ properties: {
271
+ action: { type: 'string', enum: ['list', 'create', 'update', 'delete', 'status'], description: 'Action to perform' },
272
+ summary: { type: 'string', description: 'Event title (for create/update)' },
273
+ description: { type: 'string', description: 'Event description (for create/update)' },
274
+ location: { type: 'string', description: 'Event location (for create/update)' },
275
+ start: { type: 'string', description: 'Start datetime in ISO 8601 format (for create/update)' },
276
+ end: { type: 'string', description: 'End datetime in ISO 8601 format (for create/update). Defaults to 1 hour after start.' },
277
+ eventId: { type: 'string', description: 'Event ID (for update/delete)' },
278
+ timeMin: { type: 'string', description: 'List events starting from this ISO datetime (default: now)' },
279
+ timeMax: { type: 'string', description: 'List events up to this ISO datetime' },
280
+ maxResults: { type: 'number', description: 'Max events to return (default: 20, max: 50)' },
281
+ },
282
+ required: ['action'],
283
+ },
284
+ execute: async (args) => executeCalendar(args),
285
+ },
286
+ ],
287
+ ui: {
288
+ settingsFields: [
289
+ {
290
+ key: 'provider',
291
+ label: 'Calendar Provider',
292
+ type: 'select',
293
+ options: [
294
+ { value: 'google', label: 'Google Calendar' },
295
+ { value: 'outlook', label: 'Microsoft Outlook' },
296
+ ],
297
+ defaultValue: 'google',
298
+ },
299
+ {
300
+ key: 'accessToken',
301
+ label: 'Access Token',
302
+ type: 'secret',
303
+ required: true,
304
+ placeholder: 'ya29.a0...',
305
+ help: 'OAuth2 access token for the calendar API. For Google: generate via OAuth2 playground or a service account.',
306
+ },
307
+ {
308
+ key: 'refreshToken',
309
+ label: 'Refresh Token (Google)',
310
+ type: 'secret',
311
+ placeholder: '1//0e...',
312
+ help: 'Google OAuth2 refresh token. When set, the plugin auto-refreshes expired access tokens.',
313
+ },
314
+ {
315
+ key: 'clientId',
316
+ label: 'Client ID (Google)',
317
+ type: 'text',
318
+ placeholder: '123456789.apps.googleusercontent.com',
319
+ help: 'Google OAuth2 client ID. Required for token refresh.',
320
+ },
321
+ {
322
+ key: 'clientSecret',
323
+ label: 'Client Secret (Google)',
324
+ type: 'secret',
325
+ placeholder: 'GOCSPX-...',
326
+ help: 'Google OAuth2 client secret. Required for token refresh.',
327
+ },
328
+ {
329
+ key: 'calendarId',
330
+ label: 'Calendar ID',
331
+ type: 'text',
332
+ defaultValue: 'primary',
333
+ placeholder: 'primary',
334
+ help: 'Google Calendar ID (default: "primary"). For Outlook, this is ignored.',
335
+ },
336
+ ],
337
+ },
338
+ }
339
+
340
+ getPluginManager().registerBuiltin('calendar', CalendarPlugin)
341
+
342
+ export function buildCalendarTools(bctx: ToolBuildContext): StructuredToolInterface[] {
343
+ if (!bctx.hasPlugin('calendar')) return []
344
+
345
+ return [
346
+ tool(
347
+ async (args) => executeCalendar(args),
348
+ {
349
+ name: 'calendar',
350
+ description: CalendarPlugin.tools![0].description,
351
+ schema: z.object({
352
+ action: z.enum(['list', 'create', 'update', 'delete', 'status']).describe('Action to perform'),
353
+ summary: z.string().optional().describe('Event title'),
354
+ description: z.string().optional().describe('Event description'),
355
+ location: z.string().optional().describe('Event location'),
356
+ start: z.string().optional().describe('Start datetime (ISO 8601)'),
357
+ end: z.string().optional().describe('End datetime (ISO 8601)'),
358
+ eventId: z.string().optional().describe('Event ID (for update/delete)'),
359
+ timeMin: z.string().optional().describe('List events from this datetime'),
360
+ timeMax: z.string().optional().describe('List events until this datetime'),
361
+ maxResults: z.number().optional().describe('Max results (default 20, max 50)'),
362
+ }),
363
+ },
364
+ ),
365
+ ]
366
+ }
@@ -88,7 +88,7 @@ getPluginManager().registerBuiltin('canvas', CanvasPlugin)
88
88
  * Legacy Bridge
89
89
  */
90
90
  export function buildCanvasTools(bctx: ToolBuildContext): StructuredToolInterface[] {
91
- if (!bctx.hasTool('canvas')) return []
91
+ if (!bctx.hasPlugin('canvas')) return []
92
92
  return [
93
93
  tool(
94
94
  async (args) => executeCanvasAction(args, { sessionId: bctx.ctx?.sessionId || undefined }),
@@ -107,7 +107,9 @@ async function executeChatroomAction(args: Record<string, unknown>, context: { a
107
107
  const ChatroomPlugin: Plugin = {
108
108
  name: 'Core Chatrooms',
109
109
  description: 'Manage SwarmClaw routing rules and multi-agent chatrooms.',
110
- hooks: {} as PluginHooks,
110
+ hooks: {
111
+ getCapabilityDescription: () => 'I can create and participate in chatrooms (`manage_chatrooms`) for multi-agent collaboration with @mention-based discussions.',
112
+ } as PluginHooks,
111
113
  tools: [
112
114
  {
113
115
  name: 'manage_chatrooms',
@@ -134,7 +136,7 @@ getPluginManager().registerBuiltin('chatroom', ChatroomPlugin)
134
136
  * Legacy Bridge
135
137
  */
136
138
  export function buildChatroomTools(bctx: ToolBuildContext): StructuredToolInterface[] {
137
- if (!bctx.hasTool('manage_chatrooms')) return []
139
+ if (!bctx.hasPlugin('manage_chatrooms')) return []
138
140
  return [
139
141
  tool(
140
142
  async (args) => executeChatroomAction(args, { agentId: bctx.ctx?.agentId }),
@@ -60,8 +60,6 @@ function isAutonomousSystemTurn(userText: string): boolean {
60
60
  if (!userText) return false
61
61
  const text = userText.toUpperCase()
62
62
  return text.includes('AGENT_HEARTBEAT_WAKE')
63
- || text.includes('SWARM_MAIN_MISSION_TICK')
64
- || text.includes('SWARM_MAIN_AUTO_FOLLOWUP')
65
63
  || text.includes('SWARM_HEARTBEAT_CHECK')
66
64
  }
67
65
 
@@ -247,6 +245,9 @@ interface ConnectorActionInput {
247
245
  platform?: string
248
246
  to?: string
249
247
  message?: string
248
+ messageId?: string
249
+ targetMessage?: 'last_inbound' | 'last_outbound'
250
+ emoji?: string
250
251
  voiceText?: string
251
252
  voiceId?: string
252
253
  imageUrl?: string
@@ -255,9 +256,12 @@ interface ConnectorActionInput {
255
256
  mimeType?: string
256
257
  fileName?: string
257
258
  caption?: string
259
+ replyToMessageId?: string
260
+ threadId?: string
258
261
  delaySec?: number
259
262
  followUpMessage?: string
260
263
  followUpDelaySec?: number
264
+ dedupeKey?: string
261
265
  approved?: boolean
262
266
  ptt?: boolean
263
267
  }
@@ -284,13 +288,25 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
284
288
  mimeType,
285
289
  fileName,
286
290
  caption,
291
+ messageId,
292
+ targetMessage,
293
+ emoji,
294
+ replyToMessageId,
295
+ threadId,
296
+ dedupeKey,
287
297
  approved,
288
298
  ptt,
289
299
  } = normalized as ConnectorActionInput
290
300
 
291
301
  try {
292
302
  const actionName = String(action)
293
- const { listRunningConnectors, sendConnectorMessage, getConnectorRecentChannelId } = await import('../connectors/manager')
303
+ const {
304
+ listRunningConnectors,
305
+ sendConnectorMessage,
306
+ getConnectorRecentChannelId,
307
+ scheduleConnectorFollowUp,
308
+ performConnectorMessageAction,
309
+ } = await import('../connectors/manager')
294
310
  const running = listRunningConnectors(platform || undefined)
295
311
 
296
312
  if (actionName === 'list_running' || actionName === 'list_targets') {
@@ -342,6 +358,9 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
342
358
  return { selected, connector }
343
359
  }
344
360
 
361
+ const currentSession = bctx.resolveCurrentSession?.()
362
+ const sessionId = bctx.ctx?.sessionId || currentSession?.id || undefined
363
+
345
364
  if (actionName === 'send' || actionName === 'send_voice_note' || actionName === 'schedule_followup') {
346
365
  const settings = loadSettings()
347
366
  if (settings.safetyRequireApprovalForOutbound === true && approved !== true) {
@@ -363,9 +382,7 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
363
382
  let channelId = target.channelId
364
383
  if (connector.platform === 'whatsapp') channelId = normalizeWhatsAppTarget(channelId)
365
384
 
366
- const currentSession = bctx.resolveCurrentSession?.()
367
385
  const latestUserTurn = parseLatestUserTurn(currentSession)
368
- const sessionId = bctx.ctx?.sessionId || currentSession?.id || 'unknown-session'
369
386
  const turnKey = buildConnectorActionKey([sessionId, latestUserTurn.time || 'no-user-turn'])
370
387
  const multiOutboundAllowed = userExplicitlyWantsMultipleOutbound(latestUserTurn.text)
371
388
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -392,6 +409,9 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
392
409
  const sent = await sendConnectorMessage({
393
410
  connectorId: selected.id, channelId, text: '', mediaPath: voicePath, mimeType: 'audio/mpeg',
394
411
  fileName: fileName?.trim() || 'voicenote.mp3', caption: caption?.trim() || undefined, ptt: ptt ?? true,
412
+ sessionId,
413
+ replyToMessageId: replyToMessageId?.trim() || undefined,
414
+ threadId: threadId?.trim() || undefined,
395
415
  })
396
416
  const result = JSON.stringify({ status: 'voice_sent', connectorId: sent.connectorId, platform: sent.platform, to: sent.channelId, voiceFile: voicePath })
397
417
  connectorTurnSendBudget.set(turnKey, { count: (existingBudget?.count || 0) + 1, at: now, lastResult: result })
@@ -405,11 +425,54 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
405
425
  return 'Error: message or media required.'
406
426
  }
407
427
 
428
+ if (actionName === 'schedule_followup') {
429
+ const followupText = (normalized.followUpMessage as string | undefined)?.trim() || message?.trim() || ''
430
+ if (!followupText && !media.mediaPath && !media.imageUrl && !media.fileUrl) {
431
+ return 'Error: follow-up message or media required.'
432
+ }
433
+ const followupDelay = (() => {
434
+ const direct = Number(normalized.followUpDelaySec)
435
+ if (Number.isFinite(direct) && direct >= 0) return direct
436
+ const fallback = Number(normalized.delaySec)
437
+ if (Number.isFinite(fallback) && fallback >= 0) return fallback
438
+ return 300
439
+ })()
440
+ const scheduled = scheduleConnectorFollowUp({
441
+ connectorId: selected.id,
442
+ channelId,
443
+ text: followupText,
444
+ sessionId,
445
+ delaySec: followupDelay,
446
+ dedupeKey: dedupeKey?.trim() || undefined,
447
+ imageUrl: media.imageUrl,
448
+ fileUrl: media.fileUrl,
449
+ mediaPath: media.mediaPath,
450
+ mimeType: mimeType?.trim() || undefined,
451
+ fileName: fileName?.trim() || undefined,
452
+ caption: caption?.trim() || undefined,
453
+ replyToMessageId: replyToMessageId?.trim() || undefined,
454
+ threadId: threadId?.trim() || undefined,
455
+ ptt: ptt ?? undefined,
456
+ })
457
+ return JSON.stringify({
458
+ status: 'scheduled',
459
+ connectorId: selected.id,
460
+ platform: selected.platform,
461
+ to: channelId,
462
+ followUpId: scheduled.followUpId,
463
+ sendAt: scheduled.sendAt,
464
+ })
465
+ }
466
+
408
467
  const sent = await sendConnectorMessage({
409
468
  connectorId: selected.id, channelId, text: message?.trim() || '',
469
+ sessionId,
410
470
  imageUrl: media.imageUrl, fileUrl: media.fileUrl, mediaPath: media.mediaPath,
411
471
  mimeType: mimeType?.trim() || undefined, fileName: fileName?.trim() || undefined,
412
- caption: caption?.trim() || undefined, ptt: ptt ?? undefined,
472
+ caption: caption?.trim() || undefined,
473
+ replyToMessageId: replyToMessageId?.trim() || undefined,
474
+ threadId: threadId?.trim() || undefined,
475
+ ptt: ptt ?? undefined,
413
476
  })
414
477
 
415
478
  const result = JSON.stringify({ status: 'sent', connectorId: sent.connectorId, platform: sent.platform, to: sent.channelId, messageId: sent.messageId || null })
@@ -417,6 +480,35 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
417
480
  return result
418
481
  }
419
482
 
483
+ if (actionName === 'react' || actionName === 'edit' || actionName === 'delete' || actionName === 'pin') {
484
+ const resolved = resolveSelectedConnector()
485
+ if ('error' in resolved) return resolved.error
486
+ const { selected } = resolved
487
+ const target = pickChannelTarget({
488
+ connector: resolved.connector,
489
+ to,
490
+ recentChannelId: getConnectorRecentChannelId(selected.id),
491
+ })
492
+ if (target.error) return target.error
493
+ const result = await performConnectorMessageAction({
494
+ connectorId: selected.id,
495
+ channelId: selected.platform === 'whatsapp' ? normalizeWhatsAppTarget(target.channelId) : target.channelId,
496
+ action: actionName,
497
+ messageId: messageId?.trim() || undefined,
498
+ emoji: emoji?.trim() || undefined,
499
+ text: message?.trim() || undefined,
500
+ sessionId,
501
+ targetMessage,
502
+ })
503
+ return JSON.stringify({
504
+ status: actionName,
505
+ connectorId: result.connectorId,
506
+ platform: result.platform,
507
+ to: result.channelId,
508
+ messageId: result.messageId || null,
509
+ })
510
+ }
511
+
420
512
  return 'Unknown action.'
421
513
  } catch (err: unknown) {
422
514
  return `Error: ${err instanceof Error ? err.message : String(err)}`
@@ -429,7 +521,10 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
429
521
  const ConnectorPlugin: Plugin = {
430
522
  name: 'Core Connectors',
431
523
  description: 'Manage and send messages through chat platform connectors (WhatsApp, Telegram, Slack, etc.).',
432
- hooks: {} as PluginHooks,
524
+ hooks: {
525
+ getCapabilityDescription: () => 'I can manage messaging channels (`manage_connectors`) — WhatsApp, Telegram, Slack, Discord — and send proactive messages via `connector_message_tool`.',
526
+ getOperatingGuidance: () => 'Connectors: proactive outreach for significant events only. Keep messages concise, no duplicates.',
527
+ } as PluginHooks,
433
528
  tools: [
434
529
  {
435
530
  name: 'connector_message_tool',
@@ -437,11 +532,20 @@ const ConnectorPlugin: Plugin = {
437
532
  parameters: {
438
533
  type: 'object',
439
534
  properties: {
440
- action: { type: 'string', enum: ['list_running', 'start', 'stop', 'send', 'send_voice_note'] },
535
+ action: { type: 'string', enum: ['list_running', 'start', 'stop', 'send', 'send_voice_note', 'schedule_followup', 'react', 'edit', 'delete', 'pin'] },
441
536
  connectorId: { type: 'string' },
442
537
  platform: { type: 'string' },
443
538
  to: { type: 'string' },
444
- message: { 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' },
445
549
  },
446
550
  required: ['action']
447
551
  },
@@ -456,7 +560,7 @@ getPluginManager().registerBuiltin('connectors', ConnectorPlugin)
456
560
  * Legacy Bridge
457
561
  */
458
562
  export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInterface[] {
459
- if (!bctx.hasTool('manage_connectors')) return []
563
+ if (!bctx.hasPlugin('manage_connectors')) return []
460
564
  return [
461
565
  tool(
462
566
  async (args) => executeConnectorAction(args as ConnectorActionInput, bctx),
@@ -14,25 +14,41 @@ export interface ToolContext {
14
14
  export interface SessionToolsResult {
15
15
  tools: StructuredToolInterface[]
16
16
  cleanup: () => Promise<void>
17
+ /** Maps tool name → plugin ID for attribution in usage tracking */
18
+ toolToPluginMap: Record<string, string>
17
19
  }
18
20
 
19
21
  export interface ToolBuildContext {
20
22
  cwd: string
21
23
  ctx: ToolContext | undefined
24
+ hasPlugin: (name: string) => boolean
25
+ /** @deprecated Use hasPlugin */
22
26
  hasTool: (name: string) => boolean
23
27
  cleanupFns: (() => Promise<void>)[]
24
28
  commandTimeoutMs: number
25
29
  claudeTimeoutMs: number
26
30
  cliProcessTimeoutMs: number
27
- persistDelegateResumeId: (key: 'claudeCode' | 'codex' | 'opencode', id: string | null | undefined) => void
28
- readStoredDelegateResumeId: (key: 'claudeCode' | 'codex' | 'opencode') => string | null
31
+ persistDelegateResumeId: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini', id: string | null | undefined) => void
32
+ readStoredDelegateResumeId: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini') => string | null
29
33
  resolveCurrentSession: () => any | null
30
- activeTools: string[]
34
+ activePlugins: string[]
35
+ }
36
+
37
+ function normalizeWorkspaceAlias(cwd: string, filePath: string): string {
38
+ const trimmed = filePath.trim()
39
+ if (!trimmed) return trimmed
40
+ if (trimmed === '/workspace' || trimmed === 'workspace') return cwd
41
+ if (trimmed.startsWith('/workspace/')) return trimmed.slice('/workspace/'.length)
42
+ if (trimmed.startsWith('workspace/')) return trimmed.slice('workspace/'.length)
43
+ return trimmed
31
44
  }
32
45
 
33
46
  export function safePath(cwd: string, filePath: string): string {
34
- const resolved = require('path').resolve(cwd, filePath)
35
- if (!resolved.startsWith(require('path').resolve(cwd))) {
47
+ const path = require('path')
48
+ const normalized = normalizeWorkspaceAlias(cwd, filePath)
49
+ const resolvedRoot = path.resolve(cwd)
50
+ const resolved = path.resolve(resolvedRoot, normalized)
51
+ if (!resolved.startsWith(resolvedRoot)) {
36
52
  throw new Error('Path traversal not allowed')
37
53
  }
38
54
  return resolved