@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.
Files changed (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -7,6 +7,7 @@ import { spawnSync } from 'child_process'
7
7
  import * as cheerio from 'cheerio'
8
8
  import {
9
9
  loadAgents, saveAgents,
10
+ loadProjects, saveProjects,
10
11
  loadTasks, saveTasks,
11
12
  loadSchedules, saveSchedules,
12
13
  loadSkills, saveSkills,
@@ -20,7 +21,7 @@ import {
20
21
  decryptKey,
21
22
  } from '../storage'
22
23
  import { resolveScheduleName } from '@/lib/schedule-name'
23
- import { findDuplicateSchedule, type ScheduleLike } from '@/lib/schedule-dedupe'
24
+ import { findDuplicateSchedule, findEquivalentSchedules, type ScheduleLike } from '@/lib/schedule-dedupe'
24
25
  import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
25
26
  import {
26
27
  hasManagedAgentAssignmentInput,
@@ -31,9 +32,11 @@ import {
31
32
  } from '@/lib/server/agent-assignment'
32
33
  import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
33
34
  import { normalizeSchedulePayload } from '@/lib/server/schedule-normalization'
35
+ import { buildProjectSnapshot, ensureProjectWorkspace, normalizeProjectCreateInput, normalizeProjectPatchInput } from '@/lib/server/project-utils'
34
36
  import type { ToolBuildContext } from './context'
35
37
  import { safePath, findBinaryOnPath } from './context'
36
38
  import { normalizeToolInputArgs } from './normalize-tool-args'
39
+ import type { BoardTask } from '@/types'
37
40
 
38
41
  // ---------------------------------------------------------------------------
39
42
  // Document helpers
@@ -115,6 +118,95 @@ function deriveTaskTitle(input: { title?: unknown; description?: unknown }): str
115
118
  return compact.slice(0, 120)
116
119
  }
117
120
 
121
+ const VALID_CONNECTOR_PLATFORMS = new Set([
122
+ 'discord',
123
+ 'telegram',
124
+ 'slack',
125
+ 'whatsapp',
126
+ 'openclaw',
127
+ 'bluebubbles',
128
+ 'signal',
129
+ 'teams',
130
+ 'googlechat',
131
+ 'matrix',
132
+ 'email',
133
+ 'webchat',
134
+ 'mockmail',
135
+ ])
136
+
137
+ const VALID_CONNECTOR_STATUSES = new Set(['stopped', 'running', 'error'])
138
+
139
+ function normalizeConnectorConfig(raw: unknown): Record<string, string> {
140
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}
141
+ const normalized: Record<string, string> = {}
142
+ for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
143
+ const normalizedKey = typeof key === 'string' ? key.trim() : ''
144
+ if (!normalizedKey) continue
145
+ if (typeof value === 'string') {
146
+ normalized[normalizedKey] = value
147
+ continue
148
+ }
149
+ if (typeof value === 'number' || typeof value === 'boolean') {
150
+ normalized[normalizedKey] = String(value)
151
+ }
152
+ }
153
+ return normalized
154
+ }
155
+
156
+ function sanitizeConnectorCrudPayload(
157
+ raw: Record<string, unknown>,
158
+ options: { forUpdate?: boolean } = {},
159
+ ): Record<string, unknown> {
160
+ const { forUpdate = false } = options
161
+ const out: Record<string, unknown> = {}
162
+ const setString = (key: 'name' | 'platform' | 'status') => {
163
+ if (!Object.prototype.hasOwnProperty.call(raw, key)) return
164
+ const value = typeof raw[key] === 'string' ? raw[key].trim() : ''
165
+ if (!value) return
166
+ if (key === 'platform' && !VALID_CONNECTOR_PLATFORMS.has(value)) return
167
+ if (key === 'status' && !VALID_CONNECTOR_STATUSES.has(value)) return
168
+ out[key] = value
169
+ }
170
+ const setNullableId = (key: 'agentId' | 'chatroomId' | 'credentialId') => {
171
+ if (!Object.prototype.hasOwnProperty.call(raw, key)) return
172
+ const value = typeof raw[key] === 'string' ? raw[key].trim() : ''
173
+ out[key] = value || null
174
+ }
175
+
176
+ setString('name')
177
+ setString('platform')
178
+ setString('status')
179
+ setNullableId('agentId')
180
+ setNullableId('chatroomId')
181
+ setNullableId('credentialId')
182
+
183
+ if (Object.prototype.hasOwnProperty.call(raw, 'config')) {
184
+ out.config = normalizeConnectorConfig(raw.config)
185
+ }
186
+
187
+ if (Object.prototype.hasOwnProperty.call(raw, 'isEnabled')) {
188
+ out.isEnabled = raw.isEnabled === true
189
+ } else if (Object.prototype.hasOwnProperty.call(raw, 'enabled')) {
190
+ out.isEnabled = raw.enabled === true
191
+ }
192
+
193
+ if (!forUpdate) {
194
+ const platform = typeof out.platform === 'string' ? out.platform : 'discord'
195
+ return {
196
+ name: typeof out.name === 'string' && out.name ? out.name : 'Unnamed Connector',
197
+ platform,
198
+ agentId: Object.prototype.hasOwnProperty.call(out, 'agentId') ? out.agentId : null,
199
+ chatroomId: Object.prototype.hasOwnProperty.call(out, 'chatroomId') ? out.chatroomId : null,
200
+ credentialId: Object.prototype.hasOwnProperty.call(out, 'credentialId') ? out.credentialId : null,
201
+ config: Object.prototype.hasOwnProperty.call(out, 'config') ? out.config : {},
202
+ isEnabled: Object.prototype.hasOwnProperty.call(out, 'isEnabled') ? out.isEnabled : false,
203
+ ...(typeof out.status === 'string' ? { status: out.status } : {}),
204
+ }
205
+ }
206
+
207
+ return out
208
+ }
209
+
118
210
  const TASK_STATUS_VALUES = new Set([
119
211
  'backlog',
120
212
  'queued',
@@ -132,6 +224,187 @@ function normalizeTaskStatusInput(status: unknown, prevStatus?: string): string
132
224
  return normalized
133
225
  }
134
226
 
227
+ function normalizeTaskIdList(value: unknown): string[] {
228
+ const rawValues = Array.isArray(value)
229
+ ? value
230
+ : typeof value === 'string'
231
+ ? value.split(',')
232
+ : []
233
+ const seen = new Set<string>()
234
+ const out: string[] = []
235
+ for (const entry of rawValues) {
236
+ const normalized = typeof entry === 'string' ? entry.trim() : ''
237
+ if (!normalized || seen.has(normalized)) continue
238
+ seen.add(normalized)
239
+ out.push(normalized)
240
+ }
241
+ return out
242
+ }
243
+
244
+ function pickFirstTaskId(value: unknown): string | null {
245
+ const ids = normalizeTaskIdList(value)
246
+ return ids[0] || null
247
+ }
248
+
249
+ function buildScheduleCreatorScope(schedule: Record<string, unknown> | null | undefined): {
250
+ agentId?: string | null
251
+ sessionId?: string | null
252
+ } | null {
253
+ if (!schedule || typeof schedule !== 'object') return null
254
+ const agentId = typeof schedule.createdByAgentId === 'string' && schedule.createdByAgentId.trim()
255
+ ? schedule.createdByAgentId.trim()
256
+ : null
257
+ const sessionId = typeof schedule.createdInSessionId === 'string' && schedule.createdInSessionId.trim()
258
+ ? schedule.createdInSessionId.trim()
259
+ : null
260
+ if (!agentId && !sessionId) return null
261
+ return { agentId, sessionId }
262
+ }
263
+
264
+ function deriveScheduleFollowupTarget(sessionId: string | null | undefined): {
265
+ followupConnectorId?: string | null
266
+ followupChannelId?: string | null
267
+ followupThreadId?: string | null
268
+ followupSenderId?: string | null
269
+ followupSenderName?: string | null
270
+ } {
271
+ const normalizedSessionId = typeof sessionId === 'string' ? sessionId.trim() : ''
272
+ if (!normalizedSessionId) return {}
273
+
274
+ const session = loadSessions()[normalizedSessionId] as {
275
+ connectorContext?: Record<string, unknown>
276
+ messages?: Array<Record<string, unknown>>
277
+ } | undefined
278
+ if (!session) return {}
279
+
280
+ const pickSourceFields = (source: Record<string, unknown> | null | undefined) => {
281
+ const connectorId = typeof source?.connectorId === 'string' ? source.connectorId.trim() : ''
282
+ const channelId = typeof source?.channelId === 'string' ? source.channelId.trim() : ''
283
+ if (!connectorId || !channelId) return {}
284
+ const threadId = typeof source?.threadId === 'string' ? source.threadId.trim() : ''
285
+ const senderId = typeof source?.senderId === 'string' ? source.senderId.trim() : ''
286
+ const senderName = typeof source?.senderName === 'string' ? source.senderName.trim() : ''
287
+ return {
288
+ followupConnectorId: connectorId,
289
+ followupChannelId: channelId,
290
+ followupThreadId: threadId || null,
291
+ followupSenderId: senderId || null,
292
+ followupSenderName: senderName || null,
293
+ }
294
+ }
295
+
296
+ const contextTarget = pickSourceFields(session.connectorContext || undefined)
297
+ if (contextTarget.followupConnectorId && contextTarget.followupChannelId) return contextTarget
298
+
299
+ const messages = Array.isArray(session.messages) ? session.messages : []
300
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
301
+ const message = messages[i]
302
+ if ((typeof message?.role === 'string' ? message.role : '') !== 'user') continue
303
+ if (message?.historyExcluded === true) continue
304
+ const messageTarget = pickSourceFields(message?.source as Record<string, unknown> | undefined)
305
+ if (messageTarget.followupConnectorId && messageTarget.followupChannelId) return messageTarget
306
+ }
307
+
308
+ return {}
309
+ }
310
+
311
+ function findRelatedScheduleIds(
312
+ schedules: Record<string, ScheduleLike>,
313
+ schedule: Record<string, unknown> | null | undefined,
314
+ opts: { ignoreId?: string | null } = {},
315
+ ): string[] {
316
+ if (!schedule || typeof schedule !== 'object') return []
317
+ const scope = buildScheduleCreatorScope(schedule)
318
+ if (!scope?.sessionId) return []
319
+ const matches = findEquivalentSchedules(schedules, {
320
+ id: typeof schedule.id === 'string' ? schedule.id : null,
321
+ agentId: typeof schedule.agentId === 'string' ? schedule.agentId : null,
322
+ taskPrompt: typeof schedule.taskPrompt === 'string' ? schedule.taskPrompt : null,
323
+ scheduleType: typeof schedule.scheduleType === 'string' ? schedule.scheduleType : null,
324
+ cron: typeof schedule.cron === 'string' ? schedule.cron : null,
325
+ intervalMs: typeof schedule.intervalMs === 'number' ? schedule.intervalMs : null,
326
+ runAt: typeof schedule.runAt === 'number' ? schedule.runAt : null,
327
+ createdByAgentId: scope.agentId,
328
+ createdInSessionId: scope.sessionId,
329
+ }, {
330
+ ignoreId: opts.ignoreId || (typeof schedule.id === 'string' ? schedule.id : null),
331
+ creatorScope: scope,
332
+ })
333
+ return Array.from(new Set(matches
334
+ .map((entry) => (typeof entry.id === 'string' ? entry.id : ''))
335
+ .filter(Boolean)))
336
+ }
337
+
338
+ function applyTaskContinuationDefaults(
339
+ parsed: Record<string, unknown>,
340
+ tasks: Record<string, BoardTask>,
341
+ explicitInput?: Record<string, unknown>,
342
+ ): string | null {
343
+ const explicit = explicitInput || parsed
344
+ const continuationTaskId = pickFirstTaskId(parsed.continueFromTaskId)
345
+ || pickFirstTaskId(parsed.followUpToTaskId)
346
+ || pickFirstTaskId(parsed.resumeFromTaskId)
347
+ const blockedBy = [
348
+ ...normalizeTaskIdList(parsed.blockedBy),
349
+ ...normalizeTaskIdList(parsed.dependsOn),
350
+ ...normalizeTaskIdList(parsed.dependsOnTaskIds),
351
+ ...normalizeTaskIdList(parsed.prerequisiteTaskIds),
352
+ ]
353
+ if (continuationTaskId && !blockedBy.includes(continuationTaskId)) {
354
+ blockedBy.unshift(continuationTaskId)
355
+ }
356
+ if (blockedBy.length > 0) parsed.blockedBy = blockedBy
357
+
358
+ if (continuationTaskId) {
359
+ const sourceTask = tasks[continuationTaskId]
360
+ if (!sourceTask) return `Error: source task "${continuationTaskId}" not found.`
361
+
362
+ if (!Object.prototype.hasOwnProperty.call(explicit, 'projectId') && typeof sourceTask.projectId === 'string' && sourceTask.projectId.trim()) {
363
+ parsed.projectId = sourceTask.projectId.trim()
364
+ }
365
+ if (
366
+ !Object.prototype.hasOwnProperty.call(explicit, 'agentId')
367
+ && !hasManagedAgentAssignmentInput(explicit)
368
+ && typeof sourceTask.agentId === 'string'
369
+ && sourceTask.agentId.trim()
370
+ ) {
371
+ parsed.agentId = sourceTask.agentId.trim()
372
+ }
373
+ if (!Object.prototype.hasOwnProperty.call(explicit, 'cwd') && typeof sourceTask.cwd === 'string' && sourceTask.cwd.trim()) {
374
+ parsed.cwd = sourceTask.cwd.trim()
375
+ }
376
+ const sourceSessionId = typeof sourceTask.checkpoint?.lastSessionId === 'string' && sourceTask.checkpoint.lastSessionId.trim()
377
+ ? sourceTask.checkpoint.lastSessionId.trim()
378
+ : typeof sourceTask.sessionId === 'string' && sourceTask.sessionId.trim()
379
+ ? sourceTask.sessionId.trim()
380
+ : ''
381
+ if (!Object.prototype.hasOwnProperty.call(explicit, 'sessionId') && sourceSessionId) {
382
+ parsed.sessionId = sourceSessionId
383
+ }
384
+
385
+ const resumeFieldMap: Array<[keyof BoardTask, string]> = [
386
+ ['cliResumeId', 'cliResumeId'],
387
+ ['cliProvider', 'cliProvider'],
388
+ ['claudeResumeId', 'claudeResumeId'],
389
+ ['codexResumeId', 'codexResumeId'],
390
+ ['opencodeResumeId', 'opencodeResumeId'],
391
+ ['geminiResumeId', 'geminiResumeId'],
392
+ ]
393
+ for (const [sourceKey, targetKey] of resumeFieldMap) {
394
+ const value = sourceTask[sourceKey]
395
+ if (Object.prototype.hasOwnProperty.call(explicit, targetKey)) continue
396
+ if (typeof value === 'string' && value.trim()) {
397
+ parsed[targetKey] = value.trim()
398
+ }
399
+ }
400
+ }
401
+
402
+ for (const aliasKey of ['continueFromTaskId', 'followUpToTaskId', 'resumeFromTaskId', 'dependsOn', 'dependsOnTaskIds', 'prerequisiteTaskIds']) {
403
+ delete parsed[aliasKey]
404
+ }
405
+ return null
406
+ }
407
+
135
408
  // ---------------------------------------------------------------------------
136
409
  // RESOURCE_DEFAULTS
137
410
  // ---------------------------------------------------------------------------
@@ -190,11 +463,7 @@ const RESOURCE_DEFAULTS: Record<string, (parsed: any) => any> = {
190
463
  ...p,
191
464
  }),
192
465
  manage_connectors: (p) => ({
193
- name: p.name || 'Unnamed Connector',
194
- platform: p.platform || 'discord',
195
- agentId: p.agentId || null,
196
- enabled: p.enabled ?? false,
197
- ...p,
466
+ ...sanitizeConnectorCrudPayload(p as Record<string, unknown>),
198
467
  }),
199
468
  manage_webhooks: (p) => ({
200
469
  name: p.name || 'Unnamed Webhook',
@@ -212,6 +481,7 @@ const RESOURCE_DEFAULTS: Record<string, (parsed: any) => any> = {
212
481
  agentIds: Array.isArray(p.agentIds) ? p.agentIds : [],
213
482
  ...p,
214
483
  }),
484
+ manage_projects: (p) => normalizeProjectCreateInput(p),
215
485
  }
216
486
 
217
487
  // ---------------------------------------------------------------------------
@@ -226,6 +496,7 @@ const PLATFORM_RESOURCES: Record<string, {
226
496
  readOnly?: boolean
227
497
  }> = {
228
498
  manage_agents: { toolId: 'manage_agents', label: 'agents', load: loadAgents, save: saveAgents },
499
+ manage_projects: { toolId: 'manage_projects', label: 'projects', load: loadProjects, save: saveProjects },
229
500
  manage_tasks: { toolId: 'manage_tasks', label: 'tasks', load: loadTasks, save: saveTasks },
230
501
  manage_schedules: { toolId: 'manage_schedules', label: 'schedules', load: loadSchedules, save: saveSchedules },
231
502
  manage_skills: { toolId: 'manage_skills', label: 'skills', load: loadSkills, save: saveSkills },
@@ -242,6 +513,14 @@ const PLATFORM_RESOURCES: Record<string, {
242
513
  export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[] {
243
514
  const tools: StructuredToolInterface[] = []
244
515
  const { cwd, ctx, hasPlugin } = bctx
516
+ const buildCrudPayload = (normalized: Record<string, unknown>, action: string | undefined, data: string | undefined): Record<string, unknown> => {
517
+ if (data) return JSON.parse(data)
518
+ if (action !== 'create' && action !== 'update') return {}
519
+ const entries = Object.entries(normalized).filter(([key]) =>
520
+ !['action', 'id', 'data', 'resource', 'input', 'args', 'arguments', 'payload', 'parameters'].includes(key),
521
+ )
522
+ return entries.length > 0 ? Object.fromEntries(entries) : {}
523
+ }
245
524
 
246
525
  // Build dynamic agent summary for tools that need agent awareness
247
526
  const assignScope = ctx?.platformAssignScope || 'self'
@@ -271,16 +550,35 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
271
550
  } else {
272
551
  description += `\n\nYou may create tasks for yourself, leave them unassigned, or delegate them to other agents. Your agent ID is "${ctx?.agentId || 'unknown'}". When delegating, set a target agent using "agentId", "assignee", "agent", "assignedAgentId", or "assigned_agent_id". Use the target agent's exact ID when possible. Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.` + agentSummary
273
552
  }
553
+ description += '\n\nCreate/update calls accept either `data` as a JSON string or direct top-level fields like `title`, `description`, `status`, `agentId`, and `projectId`.'
554
+ description += '\n\nFor follow-up work, set `continueFromTaskId` (or `followUpToTaskId`) to a prior task ID. The new task will inherit the predecessor\'s project/agent/session context, block on that task by default, and reuse its execution session when possible.'
555
+ if (ctx?.projectId) {
556
+ description += `\n\nCurrent project context: "${ctx.projectName || ctx.projectId}" (projectId "${ctx.projectId}"). Omit "projectId" to use this active project by default.`
557
+ }
558
+ } else if (toolKey === 'manage_projects') {
559
+ description += '\n\nProjects hold durable execution context for longer-lived work: objective, audience, pilot priorities, open objectives, credential requirements, and preferred heartbeat cadence.'
560
+ description += '\n\nCreate/update calls accept either `data` as a JSON string or direct top-level fields like `name`, `description`, `objective`, `audience`, `priorities`, `openObjectives`, `capabilityHints`, `credentialRequirements`, `heartbeatPrompt`, and `heartbeatIntervalSec`.'
561
+ if (ctx?.projectId) {
562
+ description += `\n\nCurrent project context: "${ctx.projectName || ctx.projectId}" (projectId "${ctx.projectId}"). For get/update/delete, you may omit "id" to target this active project.`
563
+ }
274
564
  } else if (toolKey === 'manage_agents') {
275
565
  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. Set "platformAssignScope":"all" to let an agent delegate work across the fleet; use "self" for solo execution.`
276
566
  } else if (toolKey === 'manage_schedules') {
277
567
  if (assignScope === 'self') {
278
- description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set it explicitly to yourself. You can only assign schedules to yourself. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).`
568
+ description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set it explicitly to yourself. You can only assign schedules to yourself. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. Reuse or update an existing schedule you already created in this chat instead of making a near-duplicate. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true). For one-off reminders, prefer "once"; agent-created one-off schedules are cleaned up automatically after they finish. When the user says stop/pause/cancel a reminder, pause or delete every matching schedule you created in this chat, not just one row.`
279
569
  } else {
280
- description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set "agentId" to another agent when needed. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).` + agentSummary
570
+ description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set "agentId" to another agent when needed. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. Reuse or update an existing schedule you already created in this chat instead of making a near-duplicate. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true). For one-off reminders, prefer "once"; agent-created one-off schedules are cleaned up automatically after they finish. When the user says stop/pause/cancel a reminder, pause or delete every matching schedule you created in this chat, not just one row.` + agentSummary
571
+ }
572
+ if (ctx?.projectId) {
573
+ description += `\n\nCurrent project context: "${ctx.projectName || ctx.projectId}" (projectId "${ctx.projectId}"). Omit "projectId" to use this active project by default.`
281
574
  }
282
575
  } else if (toolKey === 'manage_webhooks') {
283
576
  description += '\n\nUse `source`, `events`, `agentId`, and `secret` when creating webhooks. Inbound calls should POST to `/api/webhooks/{id}` with header `x-webhook-secret` when a secret is configured.'
577
+ } else if (toolKey === 'manage_secrets') {
578
+ description += '\n\nUse this for credential bootstrapping and durable secret storage. Create/update calls accept either `data` as JSON or direct top-level fields like `name`, `service`, `value`, `scope`, `agentIds`, and `projectId`.'
579
+ if (ctx?.projectId) {
580
+ description += `\n\nCurrent project context: "${ctx.projectName || ctx.projectId}" (projectId "${ctx.projectId}"). Omit "projectId" to link the secret to this active project.`
581
+ }
284
582
  }
285
583
 
286
584
  tools.push(
@@ -298,6 +596,11 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
298
596
  }
299
597
  try {
300
598
  if (action === 'list') {
599
+ if (toolKey === 'manage_projects') {
600
+ const values = Object.values(res.load())
601
+ .map((project: any) => buildProjectSnapshot(project))
602
+ return JSON.stringify(values)
603
+ }
301
604
  if (toolKey === 'manage_secrets') {
302
605
  const values = Object.values(res.load())
303
606
  .filter((s: any) => canAccessSecret(s))
@@ -307,6 +610,7 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
307
610
  service: s.service,
308
611
  scope: s.scope || 'global',
309
612
  agentIds: s.agentIds || [],
613
+ projectId: s.projectId || null,
310
614
  createdAt: s.createdAt,
311
615
  updatedAt: s.updatedAt,
312
616
  }))
@@ -315,40 +619,56 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
315
619
  return JSON.stringify(Object.values(res.load()))
316
620
  }
317
621
  if (action === 'get') {
318
- if (!id) return 'Error: "id" is required for get action.'
622
+ const effectiveId = id || (toolKey === 'manage_projects' ? ctx?.projectId || undefined : undefined)
623
+ if (!effectiveId) return 'Error: "id" is required for get action.'
319
624
  const all = res.load()
320
- if (!all[id]) return `Not found: ${res.label} "${id}"`
625
+ if (!all[effectiveId]) return `Not found: ${res.label} "${effectiveId}"`
626
+ if (toolKey === 'manage_projects') {
627
+ return JSON.stringify(buildProjectSnapshot(all[effectiveId]))
628
+ }
321
629
  if (toolKey === 'manage_secrets') {
322
- if (!canAccessSecret(all[id])) return 'Error: you do not have access to this secret.'
630
+ if (!canAccessSecret(all[effectiveId])) return 'Error: you do not have access to this secret.'
323
631
  let value = ''
324
632
  try {
325
- value = all[id].encryptedValue ? decryptKey(all[id].encryptedValue) : ''
633
+ value = all[effectiveId].encryptedValue ? decryptKey(all[effectiveId].encryptedValue) : ''
326
634
  } catch {
327
635
  value = ''
328
636
  }
329
637
  return JSON.stringify({
330
- id: all[id].id,
331
- name: all[id].name,
332
- service: all[id].service,
333
- scope: all[id].scope || 'global',
334
- agentIds: all[id].agentIds || [],
638
+ id: all[effectiveId].id,
639
+ name: all[effectiveId].name,
640
+ service: all[effectiveId].service,
641
+ scope: all[effectiveId].scope || 'global',
642
+ agentIds: all[effectiveId].agentIds || [],
643
+ projectId: all[effectiveId].projectId || null,
335
644
  value,
336
- createdAt: all[id].createdAt,
337
- updatedAt: all[id].updatedAt,
645
+ createdAt: all[effectiveId].createdAt,
646
+ updatedAt: all[effectiveId].updatedAt,
338
647
  })
339
648
  }
340
- return JSON.stringify(all[id])
649
+ return JSON.stringify(all[effectiveId])
341
650
  }
342
651
  if (res.readOnly) return `Cannot ${action} ${res.label} via this tool (read-only).`
343
652
  if (action === 'create') {
344
653
  const all = res.load()
345
- const raw = data ? JSON.parse(data) : {}
654
+ const raw = buildCrudPayload(normalized, action, data)
346
655
  const defaults = RESOURCE_DEFAULTS[toolKey]
347
656
  const parsed = defaults ? defaults(raw) : raw
348
657
  if (parsed && typeof parsed === 'object' && 'id' in parsed) {
349
658
  delete (parsed as Record<string, unknown>).id
350
659
  }
351
660
  const now = Date.now()
661
+ if (toolKey === 'manage_tasks') {
662
+ const continuationError = applyTaskContinuationDefaults(
663
+ parsed as Record<string, unknown>,
664
+ all as Record<string, BoardTask>,
665
+ raw as Record<string, unknown>,
666
+ )
667
+ if (continuationError) return continuationError
668
+ }
669
+ if ((toolKey === 'manage_tasks' || toolKey === 'manage_schedules' || toolKey === 'manage_secrets') && !Object.prototype.hasOwnProperty.call(parsed, 'projectId') && ctx?.projectId) {
670
+ parsed.projectId = ctx.projectId
671
+ }
352
672
  if (toolKey === 'manage_tasks' || toolKey === 'manage_schedules') {
353
673
  const agents = loadAgents()
354
674
  const resolution = resolveManagedAgentAssignment(
@@ -447,15 +767,19 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
447
767
  }
448
768
  }
449
769
  const newId = genId()
770
+ const scheduleFollowupTarget = toolKey === 'manage_schedules'
771
+ ? deriveScheduleFollowupTarget(ctx?.sessionId || null)
772
+ : {}
450
773
  const entry = {
451
774
  id: newId,
452
775
  ...parsed,
453
776
  createdByAgentId: ctx?.agentId || null,
454
777
  createdInSessionId: ctx?.sessionId || null,
778
+ ...scheduleFollowupTarget,
455
779
  createdAt: now,
456
780
  updatedAt: now,
457
781
  }
458
- let responseEntry: any = entry
782
+ let responseEntry: unknown = entry
459
783
  if (toolKey === 'manage_secrets') {
460
784
  const secretValue = typeof parsed.value === 'string' ? parsed.value : null
461
785
  if (!secretValue) return 'Error: data.value is required to create a secret.'
@@ -470,12 +794,17 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
470
794
  ...entry,
471
795
  scope: normalizedScope,
472
796
  agentIds: normalizedAgentIds,
797
+ projectId: typeof parsed.projectId === 'string' && parsed.projectId.trim() ? parsed.projectId.trim() : undefined,
473
798
  encryptedValue: encryptKey(secretValue),
474
799
  }
475
800
  delete (stored as any).value
476
801
  all[newId] = stored
477
802
  const { encryptedValue, ...safe } = stored
478
803
  responseEntry = safe
804
+ } else if (toolKey === 'manage_projects') {
805
+ all[newId] = entry
806
+ ensureProjectWorkspace(newId, entry.name)
807
+ responseEntry = buildProjectSnapshot(entry)
479
808
  } else {
480
809
  all[newId] = entry
481
810
  }
@@ -510,30 +839,42 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
510
839
  return JSON.stringify(responseEntry)
511
840
  }
512
841
  if (action === 'update') {
513
- if (!id) return 'Error: "id" is required for update action.'
842
+ const effectiveId = id || (toolKey === 'manage_projects' ? ctx?.projectId || undefined : undefined)
843
+ if (!effectiveId) return 'Error: "id" is required for update action.'
514
844
  const all = res.load()
515
- if (!all[id]) return `Not found: ${res.label} "${id}"`
516
- const parsed = data ? JSON.parse(data) : {}
517
- const prevStatus = all[id]?.status
518
- if (toolKey === 'manage_tasks' && Object.prototype.hasOwnProperty.call(parsed, 'status')) {
519
- const normalized = normalizeTaskStatusInput(parsed.status, prevStatus)
520
- if (normalized) parsed.status = normalized
521
- else delete parsed.status
845
+ if (!all[effectiveId]) return `Not found: ${res.label} "${effectiveId}"`
846
+ const previousEntry = all[effectiveId]
847
+ let affectedScheduleIds: string[] | null = null
848
+ const parsed = toolKey === 'manage_projects'
849
+ ? normalizeProjectPatchInput(buildCrudPayload(normalized, action, data))
850
+ : toolKey === 'manage_connectors'
851
+ ? sanitizeConnectorCrudPayload(buildCrudPayload(normalized, action, data), { forUpdate: true })
852
+ : buildCrudPayload(normalized, action, data)
853
+ const parsedRecord = parsed as Record<string, unknown>
854
+ if (toolKey === 'manage_tasks') {
855
+ const continuationError = applyTaskContinuationDefaults(parsedRecord, all as Record<string, BoardTask>, parsedRecord)
856
+ if (continuationError) return continuationError
857
+ }
858
+ const prevStatus = all[effectiveId]?.status
859
+ if (toolKey === 'manage_tasks' && Object.prototype.hasOwnProperty.call(parsedRecord, 'status')) {
860
+ const normalized = normalizeTaskStatusInput(parsedRecord.status, prevStatus)
861
+ if (normalized) parsedRecord.status = normalized
862
+ else delete parsedRecord.status
522
863
  }
523
- if (toolKey === 'manage_tasks' && Object.prototype.hasOwnProperty.call(parsed, 'qualityGate')) {
864
+ if (toolKey === 'manage_tasks' && Object.prototype.hasOwnProperty.call(parsedRecord, 'qualityGate')) {
524
865
  const settings = loadSettings()
525
- parsed.qualityGate = parsed.qualityGate
526
- ? normalizeTaskQualityGate(parsed.qualityGate, settings)
866
+ parsedRecord.qualityGate = parsedRecord.qualityGate
867
+ ? normalizeTaskQualityGate(parsedRecord.qualityGate, settings)
527
868
  : null
528
869
  }
529
870
  if (toolKey === 'manage_tasks' || toolKey === 'manage_schedules') {
530
871
  const agents = loadAgents()
531
- const requestedClear = Object.prototype.hasOwnProperty.call(parsed, 'agentId') && parsed.agentId == null
872
+ const requestedClear = Object.prototype.hasOwnProperty.call(parsedRecord, 'agentId') && parsedRecord.agentId == null
532
873
  const shouldResolveAssignment = requestedClear
533
- || hasManagedAgentAssignmentInput(parsed as Record<string, unknown>)
874
+ || hasManagedAgentAssignmentInput(parsedRecord)
534
875
  if (shouldResolveAssignment) {
535
876
  const resolution = resolveManagedAgentAssignment(
536
- parsed as Record<string, unknown>,
877
+ parsedRecord,
537
878
  agents,
538
879
  null,
539
880
  { allowDescription: false },
@@ -547,108 +888,167 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
547
888
  unresolvedReference: requestedClear ? null : resolution.unresolvedReference,
548
889
  isDelegation: toolKey === 'manage_tasks'
549
890
  ? isDelegationTaskPayload({
550
- ...all[id],
551
- ...parsed,
891
+ ...all[effectiveId],
892
+ ...parsedRecord,
552
893
  agentId: requestedClear ? null : resolution.agentId,
553
894
  } as Record<string, unknown>)
554
895
  : false,
555
896
  delegatorAgentId: toolKey === 'manage_tasks'
556
897
  ? resolveDelegatorAgentId({
557
- ...all[id],
558
- ...parsed,
559
- } as Record<string, unknown>, agents, ctx?.agentId || null)
898
+ ...all[effectiveId],
899
+ ...parsedRecord,
900
+ }, agents, ctx?.agentId || null)
560
901
  : null,
561
902
  })
562
903
  if (assignmentError) return assignmentError
563
- if (!requestedClear) parsed.agentId = resolution.agentId
904
+ if (!requestedClear) parsedRecord.agentId = resolution.agentId
564
905
  }
565
906
  }
566
- all[id] = { ...all[id], ...parsed, updatedAt: Date.now() }
907
+ all[effectiveId] = { ...all[effectiveId], ...parsed, updatedAt: Date.now() }
567
908
  if (toolKey === 'manage_schedules') {
568
- const normalizedSchedule = normalizeSchedulePayload(all[id] as Record<string, unknown>, {
909
+ const normalizedSchedule = normalizeSchedulePayload(all[effectiveId] as Record<string, unknown>, {
569
910
  cwd,
570
911
  now: Date.now(),
571
912
  })
572
913
  if (!normalizedSchedule.ok) return normalizedSchedule.error
573
- all[id] = {
574
- ...all[id],
914
+ all[effectiveId] = {
915
+ ...all[effectiveId],
575
916
  ...normalizedSchedule.value,
576
917
  updatedAt: Date.now(),
577
918
  }
919
+ const nextStatus = typeof all[effectiveId].status === 'string' ? all[effectiveId].status.trim().toLowerCase() : ''
920
+ if (nextStatus === 'paused' || nextStatus === 'completed' || nextStatus === 'failed') {
921
+ const relatedIds = findRelatedScheduleIds(all as Record<string, ScheduleLike>, previousEntry, {
922
+ ignoreId: effectiveId,
923
+ })
924
+ for (const relatedId of relatedIds) {
925
+ if (!all[relatedId]) continue
926
+ all[relatedId] = {
927
+ ...all[relatedId],
928
+ status: nextStatus,
929
+ updatedAt: Date.now(),
930
+ }
931
+ }
932
+ affectedScheduleIds = [effectiveId, ...relatedIds]
933
+ }
578
934
  }
579
935
  if (toolKey === 'manage_secrets') {
580
- if (!canAccessSecret(all[id])) return 'Error: you do not have access to this secret.'
581
- const nextScope = parsed.scope === 'agent'
936
+ if (!canAccessSecret(all[effectiveId])) return 'Error: you do not have access to this secret.'
937
+ const nextScope = parsedRecord.scope === 'agent'
582
938
  ? 'agent'
583
- : parsed.scope === 'global'
939
+ : parsedRecord.scope === 'global'
584
940
  ? 'global'
585
- : (all[id].scope === 'agent' ? 'agent' : 'global')
941
+ : (all[effectiveId].scope === 'agent' ? 'agent' : 'global')
586
942
  if (nextScope === 'agent') {
587
- const incomingIds = Array.isArray(parsed.agentIds)
588
- ? parsed.agentIds.filter((x: any) => typeof x === 'string')
589
- : Array.isArray(all[id].agentIds)
590
- ? all[id].agentIds
943
+ const incomingIds = Array.isArray(parsedRecord.agentIds)
944
+ ? parsedRecord.agentIds.filter((x: any) => typeof x === 'string')
945
+ : Array.isArray(all[effectiveId].agentIds)
946
+ ? all[effectiveId].agentIds
591
947
  : []
592
- all[id].agentIds = Array.from(new Set([
948
+ all[effectiveId].agentIds = Array.from(new Set([
593
949
  ...incomingIds,
594
950
  ...(ctx?.agentId ? [ctx.agentId] : []),
595
951
  ]))
596
952
  } else {
597
- all[id].agentIds = []
953
+ all[effectiveId].agentIds = []
598
954
  }
599
- all[id].scope = nextScope
600
- if (typeof parsed.value === 'string' && parsed.value.trim()) {
601
- all[id].encryptedValue = encryptKey(parsed.value)
955
+ all[effectiveId].scope = nextScope
956
+ if (Object.prototype.hasOwnProperty.call(parsedRecord, 'projectId')) {
957
+ all[effectiveId].projectId = typeof parsedRecord.projectId === 'string' && parsedRecord.projectId.trim()
958
+ ? parsedRecord.projectId.trim()
959
+ : undefined
602
960
  }
603
- delete all[id].value
961
+ if (typeof parsedRecord.value === 'string' && parsedRecord.value.trim()) {
962
+ all[effectiveId].encryptedValue = encryptKey(parsedRecord.value)
963
+ }
964
+ delete all[effectiveId].value
604
965
  }
605
966
 
606
- if (toolKey === 'manage_tasks' && all[id].status === 'completed') {
967
+ if (toolKey === 'manage_tasks' && all[effectiveId].status === 'completed') {
607
968
  const { formatValidationFailure, validateTaskCompletion } = await import('../task-validation')
608
969
  const { ensureTaskCompletionReport } = await import('../task-reports')
609
970
  const settings = loadSettings()
610
- const report = ensureTaskCompletionReport(all[id] as any)
611
- if (report?.relativePath) (all[id] as any).completionReportPath = report.relativePath
612
- const validation = validateTaskCompletion(all[id] as any, { report, settings })
613
- ;(all[id] as any).validation = validation
971
+ const report = ensureTaskCompletionReport(all[effectiveId] as any)
972
+ if (report?.relativePath) (all[effectiveId] as any).completionReportPath = report.relativePath
973
+ const validation = validateTaskCompletion(all[effectiveId] as any, { report, settings })
974
+ ;(all[effectiveId] as any).validation = validation
614
975
  if (!validation.ok) {
615
- all[id].status = 'failed'
616
- ;(all[id] as any).completedAt = null
617
- ;(all[id] as any).error = formatValidationFailure(validation.reasons).slice(0, 500)
618
- } else if ((all[id] as any).completedAt == null) {
619
- ;(all[id] as any).completedAt = Date.now()
976
+ all[effectiveId].status = 'failed'
977
+ ;(all[effectiveId] as any).completedAt = null
978
+ ;(all[effectiveId] as any).error = formatValidationFailure(validation.reasons).slice(0, 500)
979
+ } else if ((all[effectiveId] as any).completedAt == null) {
980
+ ;(all[effectiveId] as any).completedAt = Date.now()
620
981
  }
621
982
  }
622
983
 
623
984
  res.save(all)
624
- if (toolKey === 'manage_tasks' && prevStatus !== 'queued' && all[id].status === 'queued') {
985
+ if (toolKey === 'manage_projects') {
986
+ ensureProjectWorkspace(effectiveId, all[effectiveId].name)
987
+ }
988
+ if (toolKey === 'manage_tasks' && prevStatus !== 'queued' && all[effectiveId].status === 'queued') {
625
989
  const { enqueueTask } = await import('../queue')
626
- enqueueTask(id)
990
+ enqueueTask(effectiveId)
627
991
  } else if (
628
992
  toolKey === 'manage_tasks'
629
- && prevStatus !== all[id].status
630
- && (all[id].status === 'completed' || all[id].status === 'failed')
631
- && all[id].sessionId
993
+ && prevStatus !== all[effectiveId].status
994
+ && (all[effectiveId].status === 'completed' || all[effectiveId].status === 'failed')
995
+ && all[effectiveId].sessionId
632
996
  ) {
633
997
  const { disableSessionHeartbeat } = await import('../queue')
634
- disableSessionHeartbeat(all[id].sessionId)
998
+ disableSessionHeartbeat(all[effectiveId].sessionId)
635
999
  }
636
1000
  if (toolKey === 'manage_secrets') {
637
- const { encryptedValue, ...safe } = all[id]
1001
+ const { encryptedValue, ...safe } = all[effectiveId]
638
1002
  return JSON.stringify(safe)
639
1003
  }
640
- return JSON.stringify(all[id])
1004
+ if (toolKey === 'manage_projects') {
1005
+ return JSON.stringify(buildProjectSnapshot(all[effectiveId]))
1006
+ }
1007
+ if (toolKey === 'manage_schedules' && affectedScheduleIds?.length) {
1008
+ return JSON.stringify({
1009
+ ...all[effectiveId],
1010
+ affectedScheduleIds,
1011
+ })
1012
+ }
1013
+ return JSON.stringify(all[effectiveId])
641
1014
  }
642
1015
  if (action === 'delete') {
643
- if (!id) return 'Error: "id" is required for delete action.'
1016
+ const effectiveId = id || (toolKey === 'manage_projects' ? ctx?.projectId || undefined : undefined)
1017
+ if (!effectiveId) return 'Error: "id" is required for delete action.'
644
1018
  const all = res.load()
645
- if (!all[id]) return `Not found: ${res.label} "${id}"`
646
- if (toolKey === 'manage_secrets' && !canAccessSecret(all[id])) {
1019
+ if (!all[effectiveId]) return `Not found: ${res.label} "${effectiveId}"`
1020
+ if (toolKey === 'manage_secrets' && !canAccessSecret(all[effectiveId])) {
647
1021
  return 'Error: you do not have access to this secret.'
648
1022
  }
649
- delete all[id]
1023
+ const deletedIds = toolKey === 'manage_schedules'
1024
+ ? [effectiveId, ...findRelatedScheduleIds(all as Record<string, ScheduleLike>, all[effectiveId], { ignoreId: effectiveId })]
1025
+ : [effectiveId]
1026
+ for (const deleteId of deletedIds) {
1027
+ delete all[deleteId]
1028
+ }
650
1029
  res.save(all)
651
- return JSON.stringify({ deleted: id })
1030
+ if (toolKey === 'manage_projects') {
1031
+ const clearProjectId = (load: () => Record<string, Record<string, unknown>>, save: (d: Record<string, Record<string, unknown>>) => void) => {
1032
+ const items = load()
1033
+ let changed = false
1034
+ for (const item of Object.values(items)) {
1035
+ if (item.projectId === effectiveId) {
1036
+ item.projectId = undefined
1037
+ changed = true
1038
+ }
1039
+ }
1040
+ if (changed) save(items)
1041
+ }
1042
+ clearProjectId(loadAgents, saveAgents)
1043
+ clearProjectId(loadTasks, saveTasks)
1044
+ clearProjectId(loadSchedules, saveSchedules)
1045
+ clearProjectId(loadSkills, saveSkills)
1046
+ clearProjectId(loadSecrets, saveSecrets)
1047
+ }
1048
+ return JSON.stringify({
1049
+ deleted: effectiveId,
1050
+ deletedIds,
1051
+ })
652
1052
  }
653
1053
  return `Unknown action "${action}". Valid: list, get, create, update, delete`
654
1054
  } catch (err: any) {
@@ -662,7 +1062,7 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
662
1062
  action: z.enum(['list', 'get', 'create', 'update', 'delete']).describe('The CRUD action to perform'),
663
1063
  id: z.string().optional().describe('Resource ID (required for get, update, delete)'),
664
1064
  data: z.string().optional().describe('JSON string of fields for create/update'),
665
- }),
1065
+ }).passthrough(),
666
1066
  },
667
1067
  ),
668
1068
  )