@swarmclawai/swarmclaw 0.7.1 → 0.7.2

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 (119) hide show
  1. package/README.md +85 -139
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/thread/route.ts +1 -2
  4. package/src/app/api/agents/route.ts +1 -1
  5. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  6. package/src/app/api/{sessions → chats}/[id]/main-loop/route.ts +2 -2
  7. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  8. package/src/app/api/{sessions → chats}/[id]/route.ts +4 -52
  9. package/src/app/api/{sessions → chats}/route.ts +5 -7
  10. package/src/app/api/plugins/route.ts +3 -0
  11. package/src/app/api/plugins/settings/route.ts +35 -0
  12. package/src/app/api/usage/route.ts +30 -0
  13. package/src/cli/index.js +35 -33
  14. package/src/cli/index.ts +40 -39
  15. package/src/cli/spec.js +29 -27
  16. package/src/components/agents/agent-card.tsx +1 -1
  17. package/src/components/agents/agent-chat-list.tsx +3 -3
  18. package/src/components/agents/agent-list.tsx +8 -13
  19. package/src/components/agents/agent-sheet.tsx +2 -2
  20. package/src/components/agents/cron-job-form.tsx +3 -3
  21. package/src/components/agents/inspector-panel.tsx +2 -2
  22. package/src/components/auth/setup-wizard.tsx +5 -38
  23. package/src/components/chat/chat-area.tsx +10 -14
  24. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +3 -3
  25. package/src/components/chat/chat-header.tsx +156 -73
  26. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +4 -5
  27. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  28. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  29. package/src/components/chat/message-bubble.tsx +4 -1
  30. package/src/components/chat/message-list.tsx +2 -2
  31. package/src/components/{sessions/new-session-sheet.tsx → chat/new-chat-sheet.tsx} +6 -6
  32. package/src/components/chat/session-debug-panel.tsx +1 -1
  33. package/src/components/chat/tool-request-banner.tsx +3 -3
  34. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  35. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  36. package/src/components/connectors/connector-sheet.tsx +1 -1
  37. package/src/components/home/home-view.tsx +1 -1
  38. package/src/components/layout/app-layout.tsx +23 -2
  39. package/src/components/plugins/plugin-list.tsx +475 -254
  40. package/src/components/plugins/plugin-sheet.tsx +124 -10
  41. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  42. package/src/components/shared/command-palette.tsx +0 -1
  43. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  44. package/src/components/shared/settings/section-providers.tsx +1 -1
  45. package/src/components/shared/settings/settings-page.tsx +1 -12
  46. package/src/components/usage/metrics-dashboard.tsx +73 -0
  47. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  48. package/src/lib/chat.ts +1 -1
  49. package/src/lib/{sessions.ts → chats.ts} +28 -18
  50. package/src/lib/providers/claude-cli.ts +1 -1
  51. package/src/lib/server/approvals.ts +4 -4
  52. package/src/lib/server/capability-router.ts +10 -8
  53. package/src/lib/server/chat-execution.ts +36 -105
  54. package/src/lib/server/chatroom-helpers.ts +3 -3
  55. package/src/lib/server/connectors/manager.ts +4 -4
  56. package/src/lib/server/cost.ts +34 -1
  57. package/src/lib/server/daemon-state.ts +2 -2
  58. package/src/lib/server/heartbeat-service.ts +1 -1
  59. package/src/lib/server/main-agent-loop.ts +25 -160
  60. package/src/lib/server/main-session.ts +6 -13
  61. package/src/lib/server/orchestrator-lg.ts +3 -3
  62. package/src/lib/server/orchestrator.ts +5 -5
  63. package/src/lib/server/plugins.ts +112 -4
  64. package/src/lib/server/provider-health.ts +5 -3
  65. package/src/lib/server/queue.ts +12 -10
  66. package/src/lib/server/session-run-manager.test.ts +9 -6
  67. package/src/lib/server/session-run-manager.ts +1 -3
  68. package/src/lib/server/session-tools/calendar.ts +376 -0
  69. package/src/lib/server/session-tools/canvas.ts +1 -1
  70. package/src/lib/server/session-tools/chatroom.ts +4 -2
  71. package/src/lib/server/session-tools/connector.ts +5 -2
  72. package/src/lib/server/session-tools/context.ts +7 -3
  73. package/src/lib/server/session-tools/crud.ts +14 -6
  74. package/src/lib/server/session-tools/delegate.ts +95 -8
  75. package/src/lib/server/session-tools/discovery.ts +2 -2
  76. package/src/lib/server/session-tools/edit_file.ts +4 -2
  77. package/src/lib/server/session-tools/email.ts +322 -0
  78. package/src/lib/server/session-tools/file.ts +5 -2
  79. package/src/lib/server/session-tools/git.ts +1 -1
  80. package/src/lib/server/session-tools/http.ts +1 -1
  81. package/src/lib/server/session-tools/image-gen.ts +382 -0
  82. package/src/lib/server/session-tools/index.ts +74 -49
  83. package/src/lib/server/session-tools/memory.ts +139 -2
  84. package/src/lib/server/session-tools/monitor.ts +1 -1
  85. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  86. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  87. package/src/lib/server/session-tools/platform.ts +6 -3
  88. package/src/lib/server/session-tools/plugin-creator.ts +3 -3
  89. package/src/lib/server/session-tools/replicate.ts +303 -0
  90. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  91. package/src/lib/server/session-tools/sandbox.ts +4 -2
  92. package/src/lib/server/session-tools/schedule.ts +4 -2
  93. package/src/lib/server/session-tools/session-info.ts +7 -4
  94. package/src/lib/server/session-tools/shell.ts +5 -2
  95. package/src/lib/server/session-tools/subagent.ts +2 -2
  96. package/src/lib/server/session-tools/wallet.ts +29 -2
  97. package/src/lib/server/session-tools/web.ts +44 -5
  98. package/src/lib/server/storage.ts +29 -9
  99. package/src/lib/server/stream-agent-chat.ts +72 -249
  100. package/src/lib/server/tool-aliases.ts +26 -15
  101. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  102. package/src/lib/server/tool-capability-policy.ts +32 -27
  103. package/src/lib/tool-definitions.ts +4 -0
  104. package/src/lib/validation/schemas.ts +3 -1
  105. package/src/stores/use-app-store.ts +5 -5
  106. package/src/stores/use-chat-store.ts +7 -7
  107. package/src/types/index.ts +65 -3
  108. /package/src/app/api/{sessions → chats}/[id]/browser/route.ts +0 -0
  109. /package/src/app/api/{sessions → chats}/[id]/chat/route.ts +0 -0
  110. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  111. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  112. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  113. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  114. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  115. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  116. /package/src/app/api/{sessions → chats}/[id]/messages/route.ts +0 -0
  117. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  118. /package/src/app/api/{sessions → chats}/[id]/stop/route.ts +0 -0
  119. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -4,14 +4,14 @@ import path from 'node:path'
4
4
  import { loadTasks, saveTasks, loadQueue, saveQueue, loadAgents, loadSchedules, saveSchedules, loadSessions, saveSessions, loadSettings, loadConnectors, UPLOAD_DIR } from './storage'
5
5
  import { notify } from './ws-hub'
6
6
  import { WORKSPACE_DIR } from './data-dir'
7
- import { createOrchestratorSession, executeOrchestrator } from './orchestrator'
7
+ import { createOrchestratorSession } from './orchestrator'
8
8
  import { formatValidationFailure, validateTaskCompletion } from './task-validation'
9
9
  import { ensureTaskCompletionReport } from './task-reports'
10
10
  import { pushMainLoopEventToMainSessions } from './main-agent-loop'
11
11
  import { executeSessionChatTurn } from './chat-execution'
12
12
  import { extractTaskResult, formatResultBody } from './task-result'
13
13
  import { getCheckpointSaver } from './langgraph-checkpoint'
14
- import { isProtectedMainSession } from './main-session'
14
+ import { isMainLoopSession } from './main-session'
15
15
  import { cascadeUnblock } from './dag-validation'
16
16
  import { performGuardianRollback } from './guardian'
17
17
  import type { Agent, BoardTask, Connector, Message } from '@/types'
@@ -283,7 +283,7 @@ function pushQueueUnique(queue: string[], id: string): void {
283
283
  }
284
284
 
285
285
  function isMainSession(session: SessionLike | null | undefined): boolean {
286
- return isProtectedMainSession(session)
286
+ return isMainLoopSession(session)
287
287
  }
288
288
 
289
289
  function resolveTaskOwnerUser(task: ScheduleTaskMeta, sessions: Record<string, SessionLike>): string | null {
@@ -521,10 +521,8 @@ async function executeTaskRun(
521
521
  '- Include concrete evidence in your final summary: changed file paths, commands run, and verification results.',
522
522
  '- If blocked, state the blocker explicitly and what input or permission is missing.',
523
523
  ].join('\n')
524
- if (agent?.isOrchestrator) {
525
- return executeOrchestrator(agent, prompt, sessionId, task.id)
526
- }
527
-
524
+ // All agents (including orchestrators) go through the unified chat execution path.
525
+ // Agents with subAgentIds get delegation tools automatically via session-tools.
528
526
  const run = await executeSessionChatTurn({
529
527
  sessionId,
530
528
  message: prompt,
@@ -773,6 +771,7 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
773
771
  if (task.claudeResumeId) resumeLines.push(`Claude session: \`${task.claudeResumeId}\``)
774
772
  if (task.codexResumeId) resumeLines.push(`Codex thread: \`${task.codexResumeId}\``)
775
773
  if (task.opencodeResumeId) resumeLines.push(`OpenCode session: \`${task.opencodeResumeId}\``)
774
+ if (task.geminiResumeId) resumeLines.push(`Gemini session: \`${task.geminiResumeId}\``)
776
775
  // Fallback to legacy field
777
776
  if (resumeLines.length === 0 && task.cliResumeId) {
778
777
  resumeLines.push(`${task.cliProvider || 'CLI'} session: \`${task.cliResumeId}\``)
@@ -1315,24 +1314,27 @@ export async function processNext() {
1315
1314
  const execSession = execSessions[sessionId] as Record<string, unknown> | undefined
1316
1315
  if (execSession) {
1317
1316
  const delegateIds = execSession.delegateResumeIds as
1318
- | { claudeCode?: string | null; codex?: string | null; opencode?: string | null }
1317
+ | { claudeCode?: string | null; codex?: string | null; opencode?: string | null; gemini?: string | null }
1319
1318
  | undefined
1320
1319
  // Store each CLI resume ID separately
1321
1320
  const claudeId = (execSession.claudeSessionId as string) || delegateIds?.claudeCode || null
1322
1321
  const codexId = (execSession.codexThreadId as string) || delegateIds?.codex || null
1323
1322
  const opencodeId = (execSession.opencodeSessionId as string) || delegateIds?.opencode || null
1323
+ const geminiId = delegateIds?.gemini || null
1324
1324
  if (claudeId) t2[taskId].claudeResumeId = claudeId
1325
1325
  if (codexId) t2[taskId].codexResumeId = codexId
1326
1326
  if (opencodeId) t2[taskId].opencodeResumeId = opencodeId
1327
+ if (geminiId) t2[taskId].geminiResumeId = geminiId
1327
1328
  // Keep backward-compat single field (first available)
1328
- const primaryId = claudeId || codexId || opencodeId
1329
+ const primaryId = claudeId || codexId || opencodeId || geminiId
1329
1330
  if (primaryId) {
1330
1331
  t2[taskId].cliResumeId = primaryId
1331
1332
  if (claudeId) t2[taskId].cliProvider = 'claude-cli'
1332
1333
  else if (codexId) t2[taskId].cliProvider = 'codex-cli'
1333
1334
  else if (opencodeId) t2[taskId].cliProvider = 'opencode-cli'
1335
+ else if (geminiId) t2[taskId].cliProvider = 'gemini-cli'
1334
1336
  }
1335
- console.log(`[queue] CLI resume IDs for task ${taskId}: claude=${claudeId}, codex=${codexId}, opencode=${opencodeId}`)
1337
+ console.log(`[queue] CLI resume IDs for task ${taskId}: claude=${claudeId}, codex=${codexId}, opencode=${opencodeId}, gemini=${geminiId}`)
1336
1338
  }
1337
1339
  } catch (e) {
1338
1340
  console.warn(`[queue] Failed to extract CLI resume IDs for task ${taskId}:`, e)
@@ -3,14 +3,10 @@ import assert from 'node:assert/strict'
3
3
  import { isMainMissionSession } from './session-run-manager'
4
4
 
5
5
  describe('isMainMissionSession', () => {
6
- it('accepts explicit main sessions', () => {
7
- assert.equal(isMainMissionSession({ id: 'main-user', name: '__main__' }), true)
8
- })
9
-
10
- it('rejects human agent-thread sessions', () => {
6
+ it('accepts agent-thread sessions', () => {
11
7
  assert.equal(
12
8
  isMainMissionSession({ id: 'agent-thread-agent_coder-123', name: 'agent-thread:agent_coder', sessionType: 'human' }),
13
- false,
9
+ true,
14
10
  )
15
11
  })
16
12
 
@@ -20,4 +16,11 @@ describe('isMainMissionSession', () => {
20
16
  true,
21
17
  )
22
18
  })
19
+
20
+ it('rejects regular chat sessions', () => {
21
+ assert.equal(
22
+ isMainMissionSession({ id: 'abc123', name: 'New Chat', sessionType: 'human' }),
23
+ false,
24
+ )
25
+ })
23
26
  })
@@ -220,10 +220,8 @@ function scheduleMainLoopFollowup(sessionId: string, followup: MainLoopFollowupR
220
220
 
221
221
  export function isMainMissionSession(session: Record<string, unknown>): boolean {
222
222
  const id = typeof session.id === 'string' ? session.id.trim() : ''
223
- const name = typeof session.name === 'string' ? session.name.trim() : ''
224
223
  const sessionType = typeof session.sessionType === 'string' ? session.sessionType : ''
225
- if (id.startsWith('main-') || name === '__main__') return true
226
- // Only orchestrated thread sessions should receive autonomous main-loop followups.
224
+ if (id.startsWith('agent-thread-')) return true
227
225
  if (sessionType === 'orchestrated') return true
228
226
  return false
229
227
  }
@@ -0,0 +1,376 @@
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 { loadSettings } from '../storage'
7
+ import type { ToolBuildContext } from './context'
8
+
9
+ type CalendarProvider = 'google' | 'outlook'
10
+
11
+ interface CalendarConfig {
12
+ provider: CalendarProvider
13
+ accessToken: string
14
+ calendarId: string
15
+ refreshToken: string
16
+ clientId: string
17
+ clientSecret: string
18
+ }
19
+
20
+ function getConfig(): CalendarConfig {
21
+ const settings = loadSettings()
22
+ const ps = (settings.pluginSettings as Record<string, Record<string, unknown>> | undefined)?.calendar ?? {}
23
+ return {
24
+ provider: (ps.provider as CalendarProvider) || 'google',
25
+ accessToken: (ps.accessToken as string) || '',
26
+ calendarId: (ps.calendarId as string) || 'primary',
27
+ refreshToken: (ps.refreshToken as string) || '',
28
+ clientId: (ps.clientId as string) || '',
29
+ clientSecret: (ps.clientSecret as string) || '',
30
+ }
31
+ }
32
+
33
+ /** Try to refresh the Google OAuth access token using the refresh token. */
34
+ async function refreshGoogleToken(cfg: CalendarConfig): Promise<string | null> {
35
+ if (!cfg.refreshToken || !cfg.clientId || !cfg.clientSecret) return null
36
+ try {
37
+ const res = await fetch('https://oauth2.googleapis.com/token', {
38
+ method: 'POST',
39
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
40
+ body: new URLSearchParams({
41
+ grant_type: 'refresh_token',
42
+ refresh_token: cfg.refreshToken,
43
+ client_id: cfg.clientId,
44
+ client_secret: cfg.clientSecret,
45
+ }),
46
+ signal: AbortSignal.timeout(10_000),
47
+ })
48
+ if (!res.ok) return null
49
+ const data = await res.json()
50
+ const newToken = data?.access_token as string | undefined
51
+ if (newToken) {
52
+ // Persist the refreshed token
53
+ const settings = loadSettings()
54
+ const pluginSettings = (settings.pluginSettings as Record<string, Record<string, unknown>> | undefined) ?? {}
55
+ const calSettings = pluginSettings.calendar ?? {}
56
+ calSettings.accessToken = newToken
57
+ pluginSettings.calendar = calSettings
58
+ settings.pluginSettings = pluginSettings
59
+ const { saveSettings } = await import('../storage')
60
+ saveSettings(settings)
61
+ }
62
+ return newToken || null
63
+ } catch {
64
+ return null
65
+ }
66
+ }
67
+
68
+ async function googleRequest(method: string, urlPath: string, cfg: CalendarConfig, body?: unknown): Promise<{ ok: boolean; data?: unknown; error?: string }> {
69
+ let token = cfg.accessToken
70
+ const baseUrl = 'https://www.googleapis.com/calendar/v3'
71
+
72
+ const doFetch = async (t: string) => {
73
+ const init: RequestInit = {
74
+ method,
75
+ headers: { Authorization: `Bearer ${t}`, 'Content-Type': 'application/json' },
76
+ signal: AbortSignal.timeout(15_000),
77
+ }
78
+ if (body && method !== 'GET' && method !== 'DELETE') init.body = JSON.stringify(body)
79
+ return fetch(`${baseUrl}${urlPath}`, init)
80
+ }
81
+
82
+ let res = await doFetch(token)
83
+ if (res.status === 401) {
84
+ const refreshed = await refreshGoogleToken(cfg)
85
+ if (refreshed) {
86
+ token = refreshed
87
+ res = await doFetch(token)
88
+ }
89
+ }
90
+
91
+ if (!res.ok) {
92
+ const errText = await res.text().catch(() => '')
93
+ return { ok: false, error: `Google Calendar ${res.status}: ${errText.slice(0, 300)}` }
94
+ }
95
+ if (method === 'DELETE') return { ok: true }
96
+ const data = await res.json()
97
+ return { ok: true, data }
98
+ }
99
+
100
+ async function outlookRequest(method: string, urlPath: string, cfg: CalendarConfig, body?: unknown): Promise<{ ok: boolean; data?: unknown; error?: string }> {
101
+ const baseUrl = 'https://graph.microsoft.com/v1.0/me'
102
+ const init: RequestInit = {
103
+ method,
104
+ headers: { Authorization: `Bearer ${cfg.accessToken}`, 'Content-Type': 'application/json' },
105
+ signal: AbortSignal.timeout(15_000),
106
+ }
107
+ if (body && method !== 'GET' && method !== 'DELETE') init.body = JSON.stringify(body)
108
+ const res = await fetch(`${baseUrl}${urlPath}`, init)
109
+ if (!res.ok) {
110
+ const errText = await res.text().catch(() => '')
111
+ return { ok: false, error: `Outlook ${res.status}: ${errText.slice(0, 300)}` }
112
+ }
113
+ if (method === 'DELETE') return { ok: true }
114
+ const data = await res.json()
115
+ return { ok: true, data }
116
+ }
117
+
118
+ function formatEvent(e: Record<string, unknown>): Record<string, unknown> {
119
+ return {
120
+ id: e.id,
121
+ summary: e.summary ?? e.subject,
122
+ start: (e.start as Record<string, unknown>)?.dateTime ?? (e.start as Record<string, unknown>)?.date ?? e.start,
123
+ end: (e.end as Record<string, unknown>)?.dateTime ?? (e.end as Record<string, unknown>)?.date ?? e.end,
124
+ location: e.location ?? (e.location as unknown as Record<string, unknown>)?.displayName,
125
+ description: typeof e.description === 'string' ? e.description.slice(0, 200) : (e.body as Record<string, unknown>)?.content?.toString().slice(0, 200),
126
+ status: e.status ?? e.showAs,
127
+ htmlLink: e.htmlLink ?? e.webLink,
128
+ }
129
+ }
130
+
131
+ async function executeCalendar(args: Record<string, unknown>): Promise<string> {
132
+ const normalized = normalizeToolInputArgs(args)
133
+ const action = String(normalized.action || 'list')
134
+ const cfg = getConfig()
135
+
136
+ if (!cfg.accessToken) {
137
+ return 'Error: Calendar not configured. Ask the user to add their access token in Plugin Settings > Calendar.'
138
+ }
139
+
140
+ try {
141
+ switch (action) {
142
+ case 'list': {
143
+ const timeMin = String(normalized.timeMin || new Date().toISOString())
144
+ const timeMax = normalized.timeMax as string | undefined
145
+ const maxResults = Math.min(Number(normalized.maxResults) || 20, 50)
146
+
147
+ if (cfg.provider === 'outlook') {
148
+ const params = new URLSearchParams({
149
+ $top: String(maxResults),
150
+ $orderby: 'start/dateTime',
151
+ $filter: `start/dateTime ge '${timeMin}'${timeMax ? ` and end/dateTime le '${timeMax}'` : ''}`,
152
+ })
153
+ const r = await outlookRequest('GET', `/calendar/events?${params}`, cfg)
154
+ if (!r.ok) return `Error: ${r.error}`
155
+ const events = ((r.data as Record<string, unknown>)?.value as Record<string, unknown>[]) ?? []
156
+ return JSON.stringify(events.map(formatEvent))
157
+ }
158
+
159
+ const params = new URLSearchParams({
160
+ timeMin,
161
+ maxResults: String(maxResults),
162
+ singleEvents: 'true',
163
+ orderBy: 'startTime',
164
+ })
165
+ if (timeMax) params.set('timeMax', timeMax)
166
+ const r = await googleRequest('GET', `/calendars/${encodeURIComponent(cfg.calendarId)}/events?${params}`, cfg)
167
+ if (!r.ok) return `Error: ${r.error}`
168
+ const events = ((r.data as Record<string, unknown>)?.items as Record<string, unknown>[]) ?? []
169
+ return JSON.stringify(events.map(formatEvent))
170
+ }
171
+
172
+ case 'create': {
173
+ const summary = String(normalized.summary || normalized.title || '').trim()
174
+ if (!summary) return 'Error: "summary" (event title) is required.'
175
+ const start = String(normalized.start || '').trim()
176
+ const end = String(normalized.end || '').trim()
177
+ if (!start) return 'Error: "start" (ISO datetime) is required.'
178
+
179
+ const description = (normalized.description as string) || ''
180
+ const location = (normalized.location as string) || ''
181
+
182
+ if (cfg.provider === 'outlook') {
183
+ const body = {
184
+ subject: summary,
185
+ body: { contentType: 'text', content: description },
186
+ start: { dateTime: start, timeZone: 'UTC' },
187
+ end: { dateTime: end || new Date(new Date(start).getTime() + 3600_000).toISOString(), timeZone: 'UTC' },
188
+ location: { displayName: location },
189
+ }
190
+ const r = await outlookRequest('POST', '/calendar/events', cfg, body)
191
+ if (!r.ok) return `Error: ${r.error}`
192
+ return `Event created: ${JSON.stringify(formatEvent(r.data as Record<string, unknown>))}`
193
+ }
194
+
195
+ const body = {
196
+ summary,
197
+ description,
198
+ location,
199
+ start: { dateTime: start, timeZone: 'UTC' },
200
+ end: { dateTime: end || new Date(new Date(start).getTime() + 3600_000).toISOString(), timeZone: 'UTC' },
201
+ }
202
+ const r = await googleRequest('POST', `/calendars/${encodeURIComponent(cfg.calendarId)}/events`, cfg, body)
203
+ if (!r.ok) return `Error: ${r.error}`
204
+ return `Event created: ${JSON.stringify(formatEvent(r.data as Record<string, unknown>))}`
205
+ }
206
+
207
+ case 'update': {
208
+ const eventId = String(normalized.eventId || normalized.id || '').trim()
209
+ if (!eventId) return 'Error: "eventId" is required.'
210
+ const updates: Record<string, unknown> = {}
211
+ if (normalized.summary) updates.summary = String(normalized.summary)
212
+ if (normalized.description) updates.description = String(normalized.description)
213
+ if (normalized.location) updates.location = String(normalized.location)
214
+ if (normalized.start) updates.start = { dateTime: String(normalized.start), timeZone: 'UTC' }
215
+ if (normalized.end) updates.end = { dateTime: String(normalized.end), timeZone: 'UTC' }
216
+
217
+ if (cfg.provider === 'outlook') {
218
+ const outlookUpdates: Record<string, unknown> = {}
219
+ if (normalized.summary) outlookUpdates.subject = String(normalized.summary)
220
+ if (normalized.description) outlookUpdates.body = { contentType: 'text', content: String(normalized.description) }
221
+ if (normalized.location) outlookUpdates.location = { displayName: String(normalized.location) }
222
+ if (normalized.start) outlookUpdates.start = { dateTime: String(normalized.start), timeZone: 'UTC' }
223
+ if (normalized.end) outlookUpdates.end = { dateTime: String(normalized.end), timeZone: 'UTC' }
224
+ const r = await outlookRequest('PATCH', `/calendar/events/${eventId}`, cfg, outlookUpdates)
225
+ if (!r.ok) return `Error: ${r.error}`
226
+ return `Event updated: ${JSON.stringify(formatEvent(r.data as Record<string, unknown>))}`
227
+ }
228
+
229
+ const r = await googleRequest('PATCH', `/calendars/${encodeURIComponent(cfg.calendarId)}/events/${eventId}`, cfg, updates)
230
+ if (!r.ok) return `Error: ${r.error}`
231
+ return `Event updated: ${JSON.stringify(formatEvent(r.data as Record<string, unknown>))}`
232
+ }
233
+
234
+ case 'delete': {
235
+ const eventId = String(normalized.eventId || normalized.id || '').trim()
236
+ if (!eventId) return 'Error: "eventId" is required.'
237
+
238
+ if (cfg.provider === 'outlook') {
239
+ const r = await outlookRequest('DELETE', `/calendar/events/${eventId}`, cfg)
240
+ if (!r.ok) return `Error: ${r.error}`
241
+ return `Event ${eventId} deleted.`
242
+ }
243
+
244
+ const r = await googleRequest('DELETE', `/calendars/${encodeURIComponent(cfg.calendarId)}/events/${eventId}`, cfg)
245
+ if (!r.ok) return `Error: ${r.error}`
246
+ return `Event ${eventId} deleted.`
247
+ }
248
+
249
+ case 'status': {
250
+ return JSON.stringify({
251
+ configured: true,
252
+ provider: cfg.provider,
253
+ calendarId: cfg.calendarId,
254
+ hasRefreshToken: !!cfg.refreshToken,
255
+ })
256
+ }
257
+
258
+ default:
259
+ return `Error: Unknown action "${action}". Use: list, create, update, delete, status.`
260
+ }
261
+ } catch (err: unknown) {
262
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
263
+ }
264
+ }
265
+
266
+ const CalendarPlugin: Plugin = {
267
+ name: 'Calendar',
268
+ enabledByDefault: false,
269
+ description: 'Manage Google Calendar or Outlook calendar events — list, create, update, delete.',
270
+ hooks: {
271
+ getCapabilityDescription: () =>
272
+ 'I can manage calendar events using `calendar`: list upcoming events, create new ones, update or delete existing events. Supports Google Calendar and Outlook.',
273
+ } as PluginHooks,
274
+ tools: [
275
+ {
276
+ name: 'calendar',
277
+ description: 'Manage calendar events. Actions: list (upcoming events), create (new event), update (modify event), delete (remove event), status (check config).',
278
+ parameters: {
279
+ type: 'object',
280
+ properties: {
281
+ action: { type: 'string', enum: ['list', 'create', 'update', 'delete', 'status'], description: 'Action to perform' },
282
+ summary: { type: 'string', description: 'Event title (for create/update)' },
283
+ description: { type: 'string', description: 'Event description (for create/update)' },
284
+ location: { type: 'string', description: 'Event location (for create/update)' },
285
+ start: { type: 'string', description: 'Start datetime in ISO 8601 format (for create/update)' },
286
+ end: { type: 'string', description: 'End datetime in ISO 8601 format (for create/update). Defaults to 1 hour after start.' },
287
+ eventId: { type: 'string', description: 'Event ID (for update/delete)' },
288
+ timeMin: { type: 'string', description: 'List events starting from this ISO datetime (default: now)' },
289
+ timeMax: { type: 'string', description: 'List events up to this ISO datetime' },
290
+ maxResults: { type: 'number', description: 'Max events to return (default: 20, max: 50)' },
291
+ },
292
+ required: ['action'],
293
+ },
294
+ execute: async (args) => executeCalendar(args),
295
+ },
296
+ ],
297
+ ui: {
298
+ settingsFields: [
299
+ {
300
+ key: 'provider',
301
+ label: 'Calendar Provider',
302
+ type: 'select',
303
+ options: [
304
+ { value: 'google', label: 'Google Calendar' },
305
+ { value: 'outlook', label: 'Microsoft Outlook' },
306
+ ],
307
+ defaultValue: 'google',
308
+ },
309
+ {
310
+ key: 'accessToken',
311
+ label: 'Access Token',
312
+ type: 'secret',
313
+ required: true,
314
+ placeholder: 'ya29.a0...',
315
+ help: 'OAuth2 access token for the calendar API. For Google: generate via OAuth2 playground or a service account.',
316
+ },
317
+ {
318
+ key: 'refreshToken',
319
+ label: 'Refresh Token (Google)',
320
+ type: 'secret',
321
+ placeholder: '1//0e...',
322
+ help: 'Google OAuth2 refresh token. When set, the plugin auto-refreshes expired access tokens.',
323
+ },
324
+ {
325
+ key: 'clientId',
326
+ label: 'Client ID (Google)',
327
+ type: 'text',
328
+ placeholder: '123456789.apps.googleusercontent.com',
329
+ help: 'Google OAuth2 client ID. Required for token refresh.',
330
+ },
331
+ {
332
+ key: 'clientSecret',
333
+ label: 'Client Secret (Google)',
334
+ type: 'secret',
335
+ placeholder: 'GOCSPX-...',
336
+ help: 'Google OAuth2 client secret. Required for token refresh.',
337
+ },
338
+ {
339
+ key: 'calendarId',
340
+ label: 'Calendar ID',
341
+ type: 'text',
342
+ defaultValue: 'primary',
343
+ placeholder: 'primary',
344
+ help: 'Google Calendar ID (default: "primary"). For Outlook, this is ignored.',
345
+ },
346
+ ],
347
+ },
348
+ }
349
+
350
+ getPluginManager().registerBuiltin('calendar', CalendarPlugin)
351
+
352
+ export function buildCalendarTools(bctx: ToolBuildContext): StructuredToolInterface[] {
353
+ if (!bctx.hasPlugin('calendar')) return []
354
+
355
+ return [
356
+ tool(
357
+ async (args) => executeCalendar(args),
358
+ {
359
+ name: 'calendar',
360
+ description: CalendarPlugin.tools![0].description,
361
+ schema: z.object({
362
+ action: z.enum(['list', 'create', 'update', 'delete', 'status']).describe('Action to perform'),
363
+ summary: z.string().optional().describe('Event title'),
364
+ description: z.string().optional().describe('Event description'),
365
+ location: z.string().optional().describe('Event location'),
366
+ start: z.string().optional().describe('Start datetime (ISO 8601)'),
367
+ end: z.string().optional().describe('End datetime (ISO 8601)'),
368
+ eventId: z.string().optional().describe('Event ID (for update/delete)'),
369
+ timeMin: z.string().optional().describe('List events from this datetime'),
370
+ timeMax: z.string().optional().describe('List events until this datetime'),
371
+ maxResults: z.number().optional().describe('Max results (default 20, max 50)'),
372
+ }),
373
+ },
374
+ ),
375
+ ]
376
+ }
@@ -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 }),
@@ -429,7 +429,10 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
429
429
  const ConnectorPlugin: Plugin = {
430
430
  name: 'Core Connectors',
431
431
  description: 'Manage and send messages through chat platform connectors (WhatsApp, Telegram, Slack, etc.).',
432
- hooks: {} as PluginHooks,
432
+ hooks: {
433
+ getCapabilityDescription: () => 'I can manage messaging channels (`manage_connectors`) — WhatsApp, Telegram, Slack, Discord — and send proactive messages via `connector_message_tool`.',
434
+ getOperatingGuidance: () => 'Connectors: proactive outreach for significant events only. Keep messages concise, no duplicates.',
435
+ } as PluginHooks,
433
436
  tools: [
434
437
  {
435
438
  name: 'connector_message_tool',
@@ -456,7 +459,7 @@ getPluginManager().registerBuiltin('connectors', ConnectorPlugin)
456
459
  * Legacy Bridge
457
460
  */
458
461
  export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInterface[] {
459
- if (!bctx.hasTool('manage_connectors')) return []
462
+ if (!bctx.hasPlugin('manage_connectors')) return []
460
463
  return [
461
464
  tool(
462
465
  async (args) => executeConnectorAction(args as ConnectorActionInput, bctx),
@@ -14,20 +14,24 @@ 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[]
31
35
  }
32
36
 
33
37
  export function safePath(cwd: string, filePath: string): string {
@@ -233,12 +233,12 @@ const PLATFORM_RESOURCES: Record<string, {
233
233
 
234
234
  export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[] {
235
235
  const tools: StructuredToolInterface[] = []
236
- const { cwd, ctx, hasTool } = bctx
236
+ const { cwd, ctx, hasPlugin } = bctx
237
237
 
238
238
  // Build dynamic agent summary for tools that need agent awareness
239
239
  const assignScope = ctx?.platformAssignScope || 'self'
240
240
  let agentSummary = ''
241
- if (hasTool('manage_tasks') || hasTool('manage_schedules')) {
241
+ if (hasPlugin('manage_tasks') || hasPlugin('manage_schedules')) {
242
242
  if (assignScope === 'all') {
243
243
  try {
244
244
  const agents = loadAgents()
@@ -251,14 +251,14 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
251
251
  }
252
252
 
253
253
  for (const [toolKey, res] of Object.entries(PLATFORM_RESOURCES)) {
254
- if (!hasTool(toolKey)) continue
254
+ if (!hasPlugin(toolKey)) continue
255
255
 
256
256
  let description = `Manage SwarmClaw ${res.label}. ${res.readOnly ? 'List and get only.' : 'List, get, create, update, or delete.'} Returns JSON.`
257
257
  if (toolKey === 'manage_tasks') {
258
258
  if (assignScope === 'self') {
259
- description += `\n\nSet "agentId" to assign a task to yourself ("${ctx?.agentId || 'unknown'}") or leave it null. You can only assign tasks to yourself. Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.`
259
+ description += `\n\nDo NOT create tasks for yourself just do the work directly. Tasks are for delegating work to other agents or for user-created work items. You can only list, get, update status, or complete tasks assigned to you ("${ctx?.agentId || 'unknown'}"). Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.`
260
260
  } else {
261
- description += `\n\nSet "agentId" to assign a task to an agent (including yourself: "${ctx?.agentId || 'unknown'}"). Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.` + agentSummary
261
+ description += `\n\nDo NOT create tasks for yourself — just do the work directly. Only create tasks to delegate work to OTHER agents. Your agent ID is "${ctx?.agentId || 'unknown'}". Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.` + agentSummary
262
262
  }
263
263
  } else if (toolKey === 'manage_agents') {
264
264
  description += `\n\nAgents may self-edit their own soul. To update your soul, use action="update", id="${ctx?.agentId || 'your-agent-id'}", and include data with the "soul" field.`
@@ -396,6 +396,14 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
396
396
  agents,
397
397
  )
398
398
  }
399
+ // Agents cannot create tasks for themselves — just do the work directly.
400
+ // Tasks are for delegating to other agents or user-created work items.
401
+ if (toolKey === 'manage_tasks' && ctx?.agentId) {
402
+ const resolvedAgentId = parsed.agentId || ctx.agentId
403
+ if (resolvedAgentId === ctx.agentId) {
404
+ return 'Error: You cannot create tasks for yourself — just do the work directly. Tasks are for delegating work to other agents. If you need to track progress, use memory instead.'
405
+ }
406
+ }
399
407
  if (toolKey === 'manage_tasks') {
400
408
  parsed.title = deriveTaskTitle(parsed)
401
409
  if (!parsed.title || /^untitled task$/i.test(parsed.title)) {
@@ -598,7 +606,7 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
598
606
  )
599
607
  }
600
608
 
601
- if (hasTool('manage_documents')) {
609
+ if (hasPlugin('manage_documents')) {
602
610
  tools.push(
603
611
  tool(
604
612
  async (rawArgs) => {