@swarmclawai/swarmclaw 0.7.2 → 0.7.4

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 (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -0,0 +1,170 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import { describe, it } from 'node:test'
7
+
8
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
9
+
10
+ function runWithTempDataDir(script: string) {
11
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-discovery-approval-'))
12
+ try {
13
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
+ cwd: repoRoot,
15
+ env: {
16
+ ...process.env,
17
+ DATA_DIR: path.join(tempDir, 'data'),
18
+ WORKSPACE_DIR: path.join(tempDir, 'workspace'),
19
+ },
20
+ encoding: 'utf-8',
21
+ })
22
+ assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
23
+ const lines = (result.stdout || '')
24
+ .trim()
25
+ .split('\n')
26
+ .map((line) => line.trim())
27
+ .filter(Boolean)
28
+ const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
29
+ return JSON.parse(jsonLine || '{}')
30
+ } finally {
31
+ fs.rmSync(tempDir, { recursive: true, force: true })
32
+ }
33
+ }
34
+
35
+ describe('discovery approval flows', () => {
36
+ it('request_tool_access creates a real approval and grants the tool when auto-approved', () => {
37
+ const output = runWithTempDataDir(`
38
+ const storageMod = await import('./src/lib/server/storage.ts')
39
+ const toolsMod = await import('./src/lib/server/session-tools/index.ts')
40
+ const storage = storageMod.default || storageMod
41
+ const toolsApi = toolsMod.default || toolsMod
42
+
43
+ storage.saveSettings({ approvalsEnabled: false })
44
+
45
+ const now = Date.now()
46
+ storage.saveSessions({
47
+ session_tools: {
48
+ id: 'session_tools',
49
+ name: 'Tool Access Test',
50
+ cwd: process.env.WORKSPACE_DIR,
51
+ user: 'tester',
52
+ provider: 'openai',
53
+ model: 'gpt-test',
54
+ claudeSessionId: null,
55
+ messages: [],
56
+ createdAt: now,
57
+ lastActiveAt: now,
58
+ sessionType: 'human',
59
+ agentId: 'default',
60
+ plugins: [],
61
+ },
62
+ })
63
+
64
+ const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, [], {
65
+ sessionId: 'session_tools',
66
+ agentId: 'default',
67
+ platformAssignScope: 'self',
68
+ })
69
+ const tool = built.tools.find((entry) => entry.name === 'request_tool_access')
70
+ const raw = await tool.invoke({ toolId: 'shell', reason: 'Need terminal access.' })
71
+ const approvals = storage.loadApprovals()
72
+ const session = storage.loadSessions().session_tools
73
+ console.log(JSON.stringify({
74
+ raw,
75
+ approvalCount: Object.keys(approvals).length,
76
+ plugins: session.plugins || [],
77
+ }))
78
+ `)
79
+
80
+ assert.match(String(output.raw), /auto-approved|granted/i)
81
+ assert.equal(output.approvalCount, 1)
82
+ assert.equal(output.plugins.includes('shell'), true)
83
+ })
84
+
85
+ it('manage_capabilities request_access accepts query aliases for pluginId', () => {
86
+ const output = runWithTempDataDir(`
87
+ const storageMod = await import('./src/lib/server/storage.ts')
88
+ const toolsMod = await import('./src/lib/server/session-tools/index.ts')
89
+ const storage = storageMod.default || storageMod
90
+ const toolsApi = toolsMod.default || toolsMod
91
+
92
+ storage.saveSettings({ approvalsEnabled: false })
93
+
94
+ const now = Date.now()
95
+ storage.saveSessions({
96
+ session_caps: {
97
+ id: 'session_caps',
98
+ name: 'Capabilities Test',
99
+ cwd: process.env.WORKSPACE_DIR,
100
+ user: 'tester',
101
+ provider: 'openai',
102
+ model: 'gpt-test',
103
+ claudeSessionId: null,
104
+ messages: [],
105
+ createdAt: now,
106
+ lastActiveAt: now,
107
+ sessionType: 'human',
108
+ agentId: 'default',
109
+ plugins: [],
110
+ },
111
+ })
112
+
113
+ const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, [], {
114
+ sessionId: 'session_caps',
115
+ agentId: 'default',
116
+ platformAssignScope: 'self',
117
+ })
118
+ const tool = built.tools.find((entry) => entry.name === 'manage_capabilities')
119
+ const raw = await tool.invoke({ action: 'request_access', query: 'shell', reason: 'Need terminal access.' })
120
+ const session = storage.loadSessions().session_caps
121
+ console.log(JSON.stringify({
122
+ raw,
123
+ plugins: session.plugins || [],
124
+ }))
125
+ `)
126
+
127
+ assert.match(String(output.raw), /auto-approved|granted/i)
128
+ assert.equal(output.plugins.includes('shell'), true)
129
+ })
130
+
131
+ it('granting manage_schedules does not surface the manage_platform umbrella tool', () => {
132
+ const output = runWithTempDataDir(`
133
+ const storageMod = await import('./src/lib/server/storage.ts')
134
+ const toolsMod = await import('./src/lib/server/session-tools/index.ts')
135
+ const storage = storageMod.default || storageMod
136
+ const toolsApi = toolsMod.default || toolsMod
137
+
138
+ const now = Date.now()
139
+ storage.saveSessions({
140
+ session_sched: {
141
+ id: 'session_sched',
142
+ name: 'Schedule Tool Isolation',
143
+ cwd: process.env.WORKSPACE_DIR,
144
+ user: 'tester',
145
+ provider: 'openai',
146
+ model: 'gpt-test',
147
+ claudeSessionId: null,
148
+ messages: [],
149
+ createdAt: now,
150
+ lastActiveAt: now,
151
+ sessionType: 'human',
152
+ agentId: 'default',
153
+ plugins: ['manage_schedules'],
154
+ },
155
+ })
156
+
157
+ const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, ['manage_schedules'], {
158
+ sessionId: 'session_sched',
159
+ agentId: 'default',
160
+ platformAssignScope: 'self',
161
+ })
162
+ console.log(JSON.stringify({
163
+ toolNames: built.tools.map((entry) => entry.name).sort(),
164
+ }))
165
+ `)
166
+
167
+ assert.equal(output.toolNames.includes('manage_schedules'), true)
168
+ assert.equal(output.toolNames.includes('manage_platform'), false)
169
+ })
170
+ })
@@ -15,15 +15,24 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
15
15
  const normalized = normalizeToolInputArgs(args)
16
16
  const action = normalized.action
17
17
  const approved = normalized.approved
18
- const pluginId = typeof normalized.pluginId === 'string'
18
+ const explicitPluginId = typeof normalized.pluginId === 'string'
19
19
  ? normalized.pluginId.trim()
20
20
  : typeof normalized.plugin_id === 'string'
21
21
  ? normalized.plugin_id.trim()
22
- : undefined
22
+ : typeof normalized.toolId === 'string'
23
+ ? normalized.toolId.trim()
24
+ : typeof normalized.tool_id === 'string'
25
+ ? normalized.tool_id.trim()
26
+ : typeof normalized.tool === 'string'
27
+ ? normalized.tool.trim()
28
+ : typeof normalized.name === 'string'
29
+ ? normalized.name.trim()
30
+ : undefined
23
31
  const url = typeof normalized.url === 'string' ? normalized.url.trim() : undefined
24
32
  const reason = normalized.reason as string | undefined
25
33
  const manager = getPluginManager()
26
34
  const q = typeof normalized.query === 'string' ? normalized.query : ''
35
+ const pluginId = explicitPluginId || (action === 'request_access' ? q.trim() : '')
27
36
 
28
37
  console.log('[discovery] Executing action:', action, { query: q, pluginId })
29
38
 
@@ -88,7 +97,8 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
88
97
  if (bctx?.ctx?.sessionId) {
89
98
  const allSessions = loadSessions()
90
99
  const currentSession = allSessions[bctx.ctx.sessionId]
91
- if (currentSession && pluginIdMatches(currentSession.tools, pluginId)) {
100
+ const grantedTools = currentSession?.plugins || currentSession?.tools || []
101
+ if (currentSession && pluginIdMatches(grantedTools, pluginId)) {
92
102
  return JSON.stringify({
93
103
  alreadyGranted: true,
94
104
  pluginId,
@@ -96,8 +106,8 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
96
106
  })
97
107
  }
98
108
  }
99
- const { requestApproval } = await import('../approvals')
100
- requestApproval({
109
+ const { requestApprovalMaybeAutoApprove } = await import('../approvals')
110
+ const approval = await requestApprovalMaybeAutoApprove({
101
111
  category: 'tool_access',
102
112
  title: `Enable Plugin: ${pluginId}`,
103
113
  description: reason || `Agent is requesting access to the "${pluginId}" plugin.`,
@@ -105,6 +115,15 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
105
115
  agentId: bctx?.ctx?.agentId,
106
116
  sessionId: bctx?.ctx?.sessionId,
107
117
  })
118
+ if (approval.status === 'approved') {
119
+ return JSON.stringify({
120
+ alreadyGranted: true,
121
+ pluginId,
122
+ toolId: pluginId,
123
+ autoApproved: true,
124
+ message: `Access to "${pluginId}" was auto-approved and granted. Proceed to use it directly.`,
125
+ })
126
+ }
108
127
  return JSON.stringify({
109
128
  type: 'plugin_request',
110
129
  pluginId,
@@ -118,8 +137,8 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
118
137
  return JSON.stringify({ error: 'url is required for install_request.' })
119
138
  }
120
139
  if (approved !== true) {
121
- const { requestApproval } = await import('../approvals')
122
- requestApproval({
140
+ const { requestApprovalMaybeAutoApprove } = await import('../approvals')
141
+ const approval = await requestApprovalMaybeAutoApprove({
123
142
  category: 'plugin_install',
124
143
  title: `Install Plugin${pluginId ? `: ${pluginId}` : ' from URL'}`,
125
144
  description: reason || `Agent wants to install a plugin${url ? ` from ${url}` : ''}.`,
@@ -127,6 +146,15 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
127
146
  agentId: bctx?.ctx?.agentId,
128
147
  sessionId: bctx?.ctx?.sessionId,
129
148
  })
149
+ if (approval.status === 'approved') {
150
+ return JSON.stringify({
151
+ type: 'plugin_install_request',
152
+ url,
153
+ pluginId,
154
+ autoApproved: true,
155
+ message: `Plugin install from ${url} was auto-approved and has been applied.`,
156
+ })
157
+ }
130
158
  return JSON.stringify({
131
159
  type: 'plugin_install_request',
132
160
  url,
@@ -158,13 +186,13 @@ const DiscoveryPlugin: Plugin = {
158
186
  tools: [
159
187
  {
160
188
  name: 'manage_capabilities',
161
- description: 'Search for available plugins locally or in external marketplaces.',
189
+ description: 'Discover currently available tools, search marketplaces, or request access to a direct tool/plugin name with action="request_access" (for example "shell", "manage_schedules", or "delegate").',
162
190
  parameters: {
163
191
  type: 'object',
164
192
  properties: {
165
193
  action: { type: 'string', enum: ['discover', 'search_marketplace', 'request_access', 'install_request'] },
166
- query: { type: 'string', description: 'Search term for marketplace' },
167
- pluginId: { type: 'string', description: 'The ID or filename of the plugin' },
194
+ query: { type: 'string', description: 'Search term for marketplace, or the direct tool/plugin name for request_access' },
195
+ pluginId: { type: 'string', description: 'The exact tool/plugin name to request, such as "shell" or "manage_schedules"' },
168
196
  url: { type: 'string', description: 'URL for new plugin install request' },
169
197
  reason: { type: 'string', description: 'Why you need this capability' }
170
198
  },
@@ -187,8 +215,8 @@ export function buildDiscoveryTools(bctx: ToolBuildContext): StructuredToolInter
187
215
  description: DiscoveryPlugin.tools![0].description,
188
216
  schema: z.object({
189
217
  action: z.enum(['discover', 'search_marketplace', 'request_access', 'install_request']).describe('The discovery action to perform'),
190
- query: z.string().optional().describe('The search query for marketplace actions'),
191
- pluginId: z.string().optional(),
218
+ query: z.string().optional().describe('The marketplace query, or the direct tool/plugin name to request access to'),
219
+ pluginId: z.string().optional().describe('The exact tool/plugin name to request, such as "shell" or "manage_schedules"'),
192
220
  url: z.string().optional(),
193
221
  reason: z.string().describe('Why you need to perform this discovery action')
194
222
  })
@@ -0,0 +1,283 @@
1
+ import path from 'path'
2
+ import { z } from 'zod'
3
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
4
+ import { genId } from '@/lib/id'
5
+ import type { DocumentEntry, Plugin, PluginHooks } from '@/types'
6
+ import { getPluginManager } from '../plugins'
7
+ import { loadDocuments, saveDocuments } from '../storage'
8
+ import { extractDocumentArtifact } from '../document-utils'
9
+ import type { ToolBuildContext } from './context'
10
+ import { findBinaryOnPath, safePath } from './context'
11
+ import { normalizeToolInputArgs } from './normalize-tool-args'
12
+
13
+ function parseMetadataInput(value: unknown): Record<string, unknown> {
14
+ if (!value) return {}
15
+ if (typeof value === 'object' && !Array.isArray(value)) return value as Record<string, unknown>
16
+ if (typeof value === 'string' && value.trim()) {
17
+ try {
18
+ const parsed = JSON.parse(value)
19
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed as Record<string, unknown>
20
+ } catch {
21
+ return {}
22
+ }
23
+ }
24
+ return {}
25
+ }
26
+
27
+ function resolveFilePath(cwd: string, value: unknown): string {
28
+ if (typeof value !== 'string' || !value.trim()) throw new Error('filePath is required.')
29
+ return path.isAbsolute(value) ? path.resolve(value) : safePath(cwd, value)
30
+ }
31
+
32
+ function previewTables(tables: Awaited<ReturnType<typeof extractDocumentArtifact>>['tables']) {
33
+ return tables.map((table) => ({
34
+ name: table.name,
35
+ headers: table.headers,
36
+ rowCount: table.rowCount,
37
+ rows: table.rows.slice(0, 20),
38
+ truncated: table.rowCount > 20,
39
+ }))
40
+ }
41
+
42
+ function searchStoredDocuments(documents: Record<string, DocumentEntry>, query: string, limit: number) {
43
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean)
44
+ return Object.values(documents)
45
+ .map((doc) => {
46
+ const hay = `${doc.title}\n${doc.fileName}\n${doc.content}`.toLowerCase()
47
+ if (!terms.every((term) => hay.includes(term))) return null
48
+ let score = hay.includes(query.toLowerCase()) ? 10 : 0
49
+ for (const term of terms) {
50
+ let at = hay.indexOf(term)
51
+ while (at !== -1) {
52
+ score += 1
53
+ at = hay.indexOf(term, at + term.length)
54
+ }
55
+ }
56
+ const firstTerm = terms[0] || query
57
+ const at = hay.indexOf(firstTerm.toLowerCase())
58
+ const start = at >= 0 ? Math.max(0, at - 120) : 0
59
+ const end = Math.min(doc.content.length, start + 360)
60
+ return {
61
+ id: doc.id,
62
+ title: doc.title,
63
+ fileName: doc.fileName,
64
+ score,
65
+ snippet: doc.content.slice(start, end).replace(/\s+/g, ' ').trim(),
66
+ updatedAt: doc.updatedAt,
67
+ }
68
+ })
69
+ .filter((entry): entry is NonNullable<typeof entry> => !!entry)
70
+ .sort((a, b) => b.score - a.score)
71
+ .slice(0, limit)
72
+ }
73
+
74
+ async function executeDocumentAction(
75
+ args: Record<string, unknown>,
76
+ bctx: { cwd: string; sessionId?: string | null; agentId?: string | null },
77
+ ) {
78
+ const normalized = normalizeToolInputArgs(args)
79
+ const action = String(normalized.action || 'status').trim().toLowerCase()
80
+
81
+ try {
82
+ if (action === 'status') {
83
+ return JSON.stringify({
84
+ pdftotext: findBinaryOnPath('pdftotext') || null,
85
+ textutil: findBinaryOnPath('textutil') || null,
86
+ tesseract: findBinaryOnPath('tesseract') || null,
87
+ supports: ['read', 'metadata', 'ocr', 'extract_tables', 'store', 'list', 'search', 'get', 'delete'],
88
+ })
89
+ }
90
+
91
+ if (action === 'list' || action === 'list_stored') {
92
+ const documents = loadDocuments() as Record<string, DocumentEntry>
93
+ const limit = typeof normalized.limit === 'number' ? Math.max(1, Math.min(normalized.limit, 200)) : 50
94
+ return JSON.stringify(
95
+ Object.values(documents)
96
+ .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
97
+ .slice(0, limit)
98
+ .map((doc) => ({
99
+ id: doc.id,
100
+ title: doc.title,
101
+ fileName: doc.fileName,
102
+ sourcePath: doc.sourcePath,
103
+ textLength: doc.textLength,
104
+ method: doc.method,
105
+ metadata: doc.metadata || {},
106
+ createdAt: doc.createdAt,
107
+ updatedAt: doc.updatedAt,
108
+ })),
109
+ )
110
+ }
111
+
112
+ if (action === 'search' || action === 'search_stored') {
113
+ const query = typeof normalized.query === 'string' ? normalized.query.trim() : ''
114
+ if (!query) return 'Error: query is required.'
115
+ const documents = loadDocuments() as Record<string, DocumentEntry>
116
+ const limit = typeof normalized.limit === 'number' ? Math.max(1, Math.min(normalized.limit, 50)) : 10
117
+ const matches = searchStoredDocuments(documents, query, limit)
118
+ return JSON.stringify({ query, total: matches.length, matches })
119
+ }
120
+
121
+ if (action === 'get' || action === 'get_stored') {
122
+ const id = typeof normalized.id === 'string' ? normalized.id.trim() : ''
123
+ if (!id) return 'Error: id is required.'
124
+ const documents = loadDocuments() as Record<string, DocumentEntry>
125
+ const doc = documents[id]
126
+ if (!doc) return `Error: document "${id}" not found.`
127
+ return JSON.stringify({
128
+ ...doc,
129
+ content: doc.content.length > 80_000 ? `${doc.content.slice(0, 80_000)}\n... [truncated]` : doc.content,
130
+ })
131
+ }
132
+
133
+ if (action === 'delete' || action === 'delete_stored') {
134
+ const id = typeof normalized.id === 'string' ? normalized.id.trim() : ''
135
+ if (!id) return 'Error: id is required.'
136
+ const documents = loadDocuments() as Record<string, DocumentEntry>
137
+ if (!documents[id]) return `Error: document "${id}" not found.`
138
+ delete documents[id]
139
+ saveDocuments(documents)
140
+ return JSON.stringify({ ok: true, id })
141
+ }
142
+
143
+ const filePath = resolveFilePath(bctx.cwd, normalized.filePath ?? normalized.path)
144
+ const artifact = await extractDocumentArtifact(filePath, {
145
+ preferOcr: action === 'ocr' || normalized.preferOcr === true,
146
+ maxChars: typeof normalized.maxChars === 'number' ? Math.max(5_000, normalized.maxChars) : undefined,
147
+ })
148
+
149
+ if (action === 'metadata') {
150
+ return JSON.stringify({
151
+ filePath: artifact.filePath,
152
+ fileName: artifact.fileName,
153
+ ext: artifact.ext,
154
+ method: artifact.method,
155
+ metadata: artifact.metadata,
156
+ textLength: artifact.text.length,
157
+ tableCount: artifact.tables.length,
158
+ })
159
+ }
160
+
161
+ if (action === 'extract_tables') {
162
+ return JSON.stringify({
163
+ filePath: artifact.filePath,
164
+ fileName: artifact.fileName,
165
+ tableCount: artifact.tables.length,
166
+ tables: previewTables(artifact.tables),
167
+ })
168
+ }
169
+
170
+ if (action === 'store') {
171
+ if (!artifact.text.trim()) return 'Error: extracted document text is empty.'
172
+ const documents = loadDocuments() as Record<string, DocumentEntry>
173
+ const now = Date.now()
174
+ const docId = genId(8)
175
+ const entry: DocumentEntry = {
176
+ id: docId,
177
+ title: typeof normalized.title === 'string' && normalized.title.trim() ? normalized.title.trim() : artifact.fileName,
178
+ fileName: artifact.fileName,
179
+ sourcePath: artifact.filePath,
180
+ content: artifact.text,
181
+ method: artifact.method,
182
+ textLength: artifact.text.length,
183
+ metadata: {
184
+ ...artifact.metadata,
185
+ ...parseMetadataInput(normalized.metadata),
186
+ ext: artifact.ext,
187
+ tableCount: artifact.tables.length,
188
+ storedByAgentId: bctx.agentId || null,
189
+ storedInSessionId: bctx.sessionId || null,
190
+ },
191
+ createdAt: now,
192
+ updatedAt: now,
193
+ }
194
+ documents[entry.id] = entry
195
+ saveDocuments(documents)
196
+ return JSON.stringify({
197
+ id: entry.id,
198
+ title: entry.title,
199
+ fileName: entry.fileName,
200
+ textLength: entry.textLength,
201
+ method: entry.method,
202
+ metadata: entry.metadata,
203
+ })
204
+ }
205
+
206
+ if (action === 'read' || action === 'ocr') {
207
+ return JSON.stringify({
208
+ filePath: artifact.filePath,
209
+ fileName: artifact.fileName,
210
+ ext: artifact.ext,
211
+ method: artifact.method,
212
+ text: artifact.text,
213
+ textLength: artifact.text.length,
214
+ metadata: artifact.metadata,
215
+ tableCount: artifact.tables.length,
216
+ tables: previewTables(artifact.tables),
217
+ })
218
+ }
219
+
220
+ return `Error: Unknown action "${action}".`
221
+ } catch (err: unknown) {
222
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
223
+ }
224
+ }
225
+
226
+ const DocumentPlugin: Plugin = {
227
+ name: 'Document',
228
+ enabledByDefault: false,
229
+ description: 'Extract text/tables/OCR from local documents and optionally store them for later retrieval.',
230
+ hooks: {
231
+ getCapabilityDescription: () =>
232
+ 'I can parse local documents with `document`, including PDFs, office docs, OCR-able images, CSV/XLSX tables, and stored document search.',
233
+ } as PluginHooks,
234
+ tools: [
235
+ {
236
+ name: 'document',
237
+ description: 'Document parsing tool. Actions: status, read, metadata, ocr, extract_tables, store, list, list_stored, search, search_stored, get, get_stored, delete, delete_stored.',
238
+ parameters: {
239
+ type: 'object',
240
+ properties: {
241
+ action: {
242
+ type: 'string',
243
+ enum: ['status', 'read', 'metadata', 'ocr', 'extract_tables', 'store', 'list', 'list_stored', 'search', 'search_stored', 'get', 'get_stored', 'delete', 'delete_stored'],
244
+ },
245
+ filePath: { type: 'string' },
246
+ id: { type: 'string' },
247
+ title: { type: 'string' },
248
+ query: { type: 'string' },
249
+ metadata: {},
250
+ limit: { type: 'number' },
251
+ maxChars: { type: 'number' },
252
+ preferOcr: { type: 'boolean' },
253
+ },
254
+ required: ['action'],
255
+ },
256
+ execute: async (args, context) => executeDocumentAction(args, {
257
+ cwd: context.session.cwd || process.cwd(),
258
+ sessionId: context.session.id,
259
+ agentId: context.session.agentId || null,
260
+ }),
261
+ },
262
+ ],
263
+ }
264
+
265
+ getPluginManager().registerBuiltin('document', DocumentPlugin)
266
+
267
+ export function buildDocumentTools(bctx: ToolBuildContext): StructuredToolInterface[] {
268
+ if (!bctx.hasPlugin('document')) return []
269
+ return [
270
+ tool(
271
+ async (args) => executeDocumentAction(args, {
272
+ cwd: bctx.cwd,
273
+ sessionId: bctx.ctx?.sessionId || null,
274
+ agentId: bctx.ctx?.agentId || null,
275
+ }),
276
+ {
277
+ name: 'document',
278
+ description: DocumentPlugin.tools![0].description,
279
+ schema: z.object({}).passthrough(),
280
+ },
281
+ ),
282
+ ]
283
+ }
@@ -3,7 +3,6 @@ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
3
  import type { Plugin, PluginHooks } from '@/types'
4
4
  import { getPluginManager } from '../plugins'
5
5
  import { normalizeToolInputArgs } from './normalize-tool-args'
6
- import { loadSettings } from '../storage'
7
6
  import type { ToolBuildContext } from './context'
8
7
 
9
8
  interface SmtpConfig {
@@ -17,8 +16,7 @@ interface SmtpConfig {
17
16
  }
18
17
 
19
18
  function getSmtpConfig(): SmtpConfig {
20
- const settings = loadSettings()
21
- const ps = (settings.pluginSettings as Record<string, Record<string, unknown>> | undefined)?.email ?? {}
19
+ const ps = getPluginManager().getPluginSettings('email')
22
20
  return {
23
21
  host: (ps.host as string) || '',
24
22
  port: Number(ps.port) || 587,