@swarmclawai/swarmclaw 0.4.0 → 0.4.5

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 (144) hide show
  1. package/README.md +13 -2
  2. package/next.config.ts +8 -0
  3. package/package.json +2 -1
  4. package/src/app/api/agents/[id]/route.ts +20 -21
  5. package/src/app/api/agents/[id]/thread/route.ts +2 -2
  6. package/src/app/api/agents/route.ts +3 -2
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/connectors/[id]/route.ts +10 -3
  9. package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
  10. package/src/app/api/connectors/route.ts +6 -3
  11. package/src/app/api/credentials/[id]/route.ts +2 -1
  12. package/src/app/api/credentials/route.ts +2 -2
  13. package/src/app/api/documents/route.ts +2 -2
  14. package/src/app/api/files/serve/route.ts +8 -0
  15. package/src/app/api/knowledge/[id]/route.ts +5 -4
  16. package/src/app/api/knowledge/upload/route.ts +2 -2
  17. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  18. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  19. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  20. package/src/app/api/mcp-servers/route.ts +2 -2
  21. package/src/app/api/memory/[id]/route.ts +9 -8
  22. package/src/app/api/memory/route.ts +2 -2
  23. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  24. package/src/app/api/openclaw/directory/route.ts +26 -0
  25. package/src/app/api/openclaw/discover/route.ts +61 -0
  26. package/src/app/api/openclaw/sync/route.ts +30 -0
  27. package/src/app/api/orchestrator/run/route.ts +2 -2
  28. package/src/app/api/projects/[id]/route.ts +55 -0
  29. package/src/app/api/projects/route.ts +27 -0
  30. package/src/app/api/providers/[id]/models/route.ts +2 -1
  31. package/src/app/api/providers/[id]/route.ts +13 -15
  32. package/src/app/api/providers/route.ts +2 -2
  33. package/src/app/api/schedules/[id]/route.ts +16 -18
  34. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  35. package/src/app/api/schedules/route.ts +2 -2
  36. package/src/app/api/secrets/[id]/route.ts +16 -17
  37. package/src/app/api/secrets/route.ts +2 -2
  38. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  39. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  40. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  41. package/src/app/api/sessions/[id]/messages/route.ts +2 -1
  42. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  43. package/src/app/api/sessions/[id]/route.ts +2 -1
  44. package/src/app/api/sessions/route.ts +2 -2
  45. package/src/app/api/skills/[id]/route.ts +23 -21
  46. package/src/app/api/skills/import/route.ts +2 -2
  47. package/src/app/api/skills/route.ts +2 -2
  48. package/src/app/api/tasks/[id]/approve/route.ts +2 -1
  49. package/src/app/api/tasks/[id]/route.ts +6 -5
  50. package/src/app/api/tasks/route.ts +2 -2
  51. package/src/app/api/tts/stream/route.ts +48 -0
  52. package/src/app/api/upload/route.ts +2 -2
  53. package/src/app/api/uploads/[filename]/route.ts +4 -1
  54. package/src/app/api/webhooks/[id]/route.ts +29 -31
  55. package/src/app/api/webhooks/route.ts +2 -2
  56. package/src/app/page.tsx +3 -24
  57. package/src/cli/index.js +28 -0
  58. package/src/cli/index.ts +1 -1
  59. package/src/cli/spec.js +2 -0
  60. package/src/components/agents/agent-list.tsx +3 -1
  61. package/src/components/agents/agent-sheet.tsx +116 -14
  62. package/src/components/chat/chat-area.tsx +27 -4
  63. package/src/components/chat/chat-header.tsx +141 -29
  64. package/src/components/chat/tool-call-bubble.tsx +9 -3
  65. package/src/components/chat/voice-overlay.tsx +80 -0
  66. package/src/components/connectors/connector-list.tsx +6 -2
  67. package/src/components/connectors/connector-sheet.tsx +31 -7
  68. package/src/components/layout/app-layout.tsx +47 -25
  69. package/src/components/projects/project-list.tsx +122 -0
  70. package/src/components/projects/project-sheet.tsx +135 -0
  71. package/src/components/schedules/schedule-list.tsx +3 -1
  72. package/src/components/sessions/new-session-sheet.tsx +6 -6
  73. package/src/components/sessions/session-card.tsx +1 -1
  74. package/src/components/sessions/session-list.tsx +7 -7
  75. package/src/components/shared/connector-platform-icon.tsx +4 -0
  76. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  77. package/src/components/shared/settings/section-orchestrator.tsx +1 -2
  78. package/src/components/shared/settings/section-web-search.tsx +56 -0
  79. package/src/components/shared/settings/settings-page.tsx +73 -0
  80. package/src/components/skills/skill-list.tsx +2 -1
  81. package/src/components/tasks/task-list.tsx +5 -2
  82. package/src/hooks/use-continuous-speech.ts +144 -0
  83. package/src/hooks/use-view-router.ts +52 -0
  84. package/src/hooks/use-voice-conversation.ts +80 -0
  85. package/src/lib/id.ts +6 -0
  86. package/src/lib/projects.ts +13 -0
  87. package/src/lib/provider-sets.ts +5 -0
  88. package/src/lib/providers/anthropic.ts +14 -1
  89. package/src/lib/providers/index.ts +6 -0
  90. package/src/lib/providers/ollama.ts +9 -1
  91. package/src/lib/providers/openai.ts +9 -1
  92. package/src/lib/providers/openclaw.ts +11 -0
  93. package/src/lib/server/api-routes.test.ts +5 -6
  94. package/src/lib/server/build-llm.ts +17 -4
  95. package/src/lib/server/chat-execution.ts +38 -4
  96. package/src/lib/server/collection-helpers.ts +54 -0
  97. package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
  98. package/src/lib/server/connectors/bluebubbles.ts +357 -0
  99. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  100. package/src/lib/server/connectors/googlechat.ts +46 -7
  101. package/src/lib/server/connectors/manager.ts +392 -3
  102. package/src/lib/server/connectors/media.ts +2 -2
  103. package/src/lib/server/connectors/openclaw.ts +64 -0
  104. package/src/lib/server/connectors/pairing.test.ts +99 -0
  105. package/src/lib/server/connectors/pairing.ts +256 -0
  106. package/src/lib/server/connectors/signal.ts +1 -0
  107. package/src/lib/server/connectors/teams.ts +5 -5
  108. package/src/lib/server/connectors/types.ts +10 -0
  109. package/src/lib/server/execution-log.ts +3 -3
  110. package/src/lib/server/heartbeat-service.ts +1 -1
  111. package/src/lib/server/knowledge-db.test.ts +2 -33
  112. package/src/lib/server/main-agent-loop.ts +6 -6
  113. package/src/lib/server/memory-db.ts +6 -6
  114. package/src/lib/server/openclaw-approvals.ts +105 -0
  115. package/src/lib/server/openclaw-sync.ts +496 -0
  116. package/src/lib/server/orchestrator-lg.ts +30 -9
  117. package/src/lib/server/orchestrator.ts +4 -4
  118. package/src/lib/server/process-manager.ts +2 -2
  119. package/src/lib/server/queue.ts +22 -10
  120. package/src/lib/server/scheduler.ts +2 -2
  121. package/src/lib/server/session-mailbox.ts +2 -2
  122. package/src/lib/server/session-run-manager.ts +2 -2
  123. package/src/lib/server/session-tools/connector.ts +51 -4
  124. package/src/lib/server/session-tools/crud.ts +3 -3
  125. package/src/lib/server/session-tools/delegate.ts +3 -3
  126. package/src/lib/server/session-tools/file.ts +176 -3
  127. package/src/lib/server/session-tools/index.ts +2 -0
  128. package/src/lib/server/session-tools/memory.ts +2 -2
  129. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  130. package/src/lib/server/session-tools/sandbox.ts +33 -0
  131. package/src/lib/server/session-tools/search-providers.ts +270 -0
  132. package/src/lib/server/session-tools/session-info.ts +2 -2
  133. package/src/lib/server/session-tools/web.ts +47 -66
  134. package/src/lib/server/storage.ts +12 -0
  135. package/src/lib/server/stream-agent-chat.ts +29 -0
  136. package/src/lib/server/task-result.test.ts +44 -0
  137. package/src/lib/server/task-result.ts +14 -0
  138. package/src/lib/tool-definitions.ts +5 -3
  139. package/src/lib/tts-stream.ts +130 -0
  140. package/src/lib/view-routes.ts +28 -0
  141. package/src/proxy.ts +3 -0
  142. package/src/stores/use-app-store.ts +28 -1
  143. package/src/stores/use-chat-store.ts +9 -1
  144. package/src/types/index.ts +27 -2
@@ -0,0 +1,496 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import crypto from 'node:crypto'
4
+ import { DATA_DIR } from './data-dir'
5
+ import { loadSettings, loadAgents, saveAgents, loadSchedules, saveSchedules, loadCredentials, decryptKey, encryptKey } from './storage'
6
+ import { getMemoryDb } from './memory-db'
7
+ import type { AppSettings, MemoryEntry, Schedule } from '@/types'
8
+
9
+ export interface OpenClawSyncConfig {
10
+ workspacePath: string
11
+ autoSyncMemory: boolean
12
+ autoSyncSchedules: boolean
13
+ }
14
+
15
+ /** Resolve the OpenClaw workspace directory. Checks settings override, then ~/.openclaw, then ~/.clawdbot */
16
+ export function resolveOpenClawWorkspace(): string {
17
+ const settings = loadSettings() as AppSettings
18
+ if (settings.openclawWorkspacePath) {
19
+ const resolved = settings.openclawWorkspacePath.replace(/^~/, process.env.HOME || '')
20
+ if (fs.existsSync(resolved)) return resolved
21
+ }
22
+ const home = process.env.HOME || process.env.USERPROFILE || ''
23
+ const override = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim()
24
+ if (override) {
25
+ const resolved = path.resolve(override.replace(/^~/, home))
26
+ if (fs.existsSync(resolved)) return resolved
27
+ }
28
+ const newDir = path.join(home, '.openclaw')
29
+ if (fs.existsSync(newDir)) return newDir
30
+ const legacyDir = path.join(home, '.clawdbot')
31
+ if (fs.existsSync(legacyDir)) return legacyDir
32
+ // Default to creating ~/.openclaw
33
+ return newDir
34
+ }
35
+
36
+ export function loadSyncConfig(): OpenClawSyncConfig {
37
+ const settings = loadSettings() as AppSettings
38
+ return {
39
+ workspacePath: resolveOpenClawWorkspace(),
40
+ autoSyncMemory: settings.openclawAutoSyncMemory ?? false,
41
+ autoSyncSchedules: settings.openclawAutoSyncSchedules ?? false,
42
+ }
43
+ }
44
+
45
+ function ensureDir(dirPath: string): void {
46
+ fs.mkdirSync(dirPath, { recursive: true })
47
+ }
48
+
49
+ function contentHash(text: string): string {
50
+ return crypto.createHash('sha256').update(text).digest('hex').slice(0, 16)
51
+ }
52
+
53
+ // --- Memory Sync (Feature 2) ---
54
+
55
+ export function pushMemoryToOpenClaw(agentId?: string): { written: number } {
56
+ const config = loadSyncConfig()
57
+ const memoryDir = path.join(config.workspacePath, 'memory')
58
+ ensureDir(memoryDir)
59
+
60
+ const db = getMemoryDb()
61
+ const entries = db.list(agentId, 500) as MemoryEntry[]
62
+ if (!entries.length) return { written: 0 }
63
+
64
+ // Group by date
65
+ const byDate = new Map<string, MemoryEntry[]>()
66
+ for (const entry of entries) {
67
+ const date = new Date(entry.createdAt).toISOString().slice(0, 10)
68
+ const group = byDate.get(date) || []
69
+ group.push(entry)
70
+ byDate.set(date, group)
71
+ }
72
+
73
+ let written = 0
74
+ for (const [date, group] of byDate) {
75
+ const lines: string[] = [`# Memory — ${date}`, '']
76
+ for (const entry of group) {
77
+ lines.push(`## ${entry.title}`)
78
+ lines.push(`- Category: ${entry.category}`)
79
+ if (entry.agentId) lines.push(`- Agent: ${entry.agentId}`)
80
+ lines.push('')
81
+ lines.push(entry.content)
82
+ lines.push('')
83
+ }
84
+ fs.writeFileSync(path.join(memoryDir, `${date}.md`), lines.join('\n'))
85
+ written++
86
+ }
87
+
88
+ // Write curated MEMORY.md
89
+ const curated = entries
90
+ .filter((e) => e.category !== 'execution' && e.category !== 'working' && e.category !== 'scratch')
91
+ .slice(0, 50)
92
+ if (curated.length > 0) {
93
+ const memoryMdLines: string[] = ['# Memory', '']
94
+ for (const entry of curated) {
95
+ memoryMdLines.push(`- **${entry.title}** (${entry.category}): ${entry.content.slice(0, 200)}`)
96
+ }
97
+ fs.writeFileSync(path.join(config.workspacePath, 'MEMORY.md'), memoryMdLines.join('\n'))
98
+ }
99
+
100
+ return { written }
101
+ }
102
+
103
+ export function pullMemoryFromOpenClaw(): { imported: number } {
104
+ const config = loadSyncConfig()
105
+ const memoryDir = path.join(config.workspacePath, 'memory')
106
+ const db = getMemoryDb()
107
+ let imported = 0
108
+
109
+ // Build set of existing content hashes for dedup
110
+ const existing = db.list(undefined, 500) as MemoryEntry[]
111
+ const existingHashes = new Set(existing.map((e) => contentHash(`${e.title}|${e.content}`)))
112
+
113
+ const files: string[] = []
114
+ if (fs.existsSync(memoryDir)) {
115
+ files.push(...fs.readdirSync(memoryDir).filter((f) => f.endsWith('.md')))
116
+ }
117
+
118
+ // Also check MEMORY.md at workspace root
119
+ const memoryMdPath = path.join(config.workspacePath, 'MEMORY.md')
120
+ if (fs.existsSync(memoryMdPath)) {
121
+ const content = fs.readFileSync(memoryMdPath, 'utf8')
122
+ const lines = content.split('\n')
123
+ for (const line of lines) {
124
+ const match = line.match(/^- \*\*(.+?)\*\* \((.+?)\): (.+)/)
125
+ if (match) {
126
+ const [, title, category, text] = match
127
+ const hash = contentHash(`${title}|${text}`)
128
+ if (!existingHashes.has(hash)) {
129
+ db.add({
130
+ agentId: null,
131
+ sessionId: null,
132
+ category: category || 'note',
133
+ title: title || 'Imported',
134
+ content: text || '',
135
+ metadata: { source: 'openclaw-sync' },
136
+ })
137
+ existingHashes.add(hash)
138
+ imported++
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ for (const file of files) {
145
+ const content = fs.readFileSync(path.join(memoryDir, file), 'utf8')
146
+ // Parse markdown sections
147
+ const sections = content.split(/^## /m).slice(1)
148
+ for (const section of sections) {
149
+ const lines = section.split('\n')
150
+ const title = (lines[0] || 'Untitled').trim()
151
+ const bodyLines = lines.slice(1).filter((l) => !l.startsWith('- Category:') && !l.startsWith('- Agent:'))
152
+ const body = bodyLines.join('\n').trim()
153
+ if (!body) continue
154
+
155
+ const hash = contentHash(`${title}|${body}`)
156
+ if (existingHashes.has(hash)) continue
157
+
158
+ const categoryMatch = section.match(/- Category: (.+)/)
159
+ const category = categoryMatch?.[1]?.trim() || 'note'
160
+
161
+ db.add({
162
+ agentId: null,
163
+ sessionId: null,
164
+ category,
165
+ title,
166
+ content: body,
167
+ metadata: { source: 'openclaw-sync' },
168
+ })
169
+ existingHashes.add(hash)
170
+ imported++
171
+ }
172
+ }
173
+
174
+ return { imported }
175
+ }
176
+
177
+ // --- Workspace File Mapping (Feature 3) ---
178
+
179
+ export function pushAgentToOpenClaw(agentId: string): { written: string[] } {
180
+ const config = loadSyncConfig()
181
+ const agents = loadAgents()
182
+ const agent = agents[agentId]
183
+ if (!agent) throw new Error(`Agent not found: ${agentId}`)
184
+
185
+ const agentDir = path.join(config.workspacePath, 'agents', agent.name.toLowerCase().replace(/\s+/g, '-'))
186
+ ensureDir(agentDir)
187
+
188
+ const written: string[] = []
189
+
190
+ if (agent.soul) {
191
+ const soulPath = path.join(agentDir, 'SOUL.md')
192
+ fs.writeFileSync(soulPath, agent.soul)
193
+ written.push('SOUL.md')
194
+ }
195
+
196
+ const identityLines: string[] = [`# ${agent.name}`, '']
197
+ if (agent.description) identityLines.push(agent.description)
198
+ identityLines.push('')
199
+ identityLines.push(`- Provider: ${agent.provider}`)
200
+ identityLines.push(`- Model: ${agent.model}`)
201
+ if (agent.capabilities?.length) {
202
+ identityLines.push(`- Capabilities: ${agent.capabilities.join(', ')}`)
203
+ }
204
+ const identityPath = path.join(agentDir, 'IDENTITY.md')
205
+ fs.writeFileSync(identityPath, identityLines.join('\n'))
206
+ written.push('IDENTITY.md')
207
+
208
+ return { written }
209
+ }
210
+
211
+ export function pullAgentFromOpenClaw(agentId: string): { updated: string[] } {
212
+ const config = loadSyncConfig()
213
+ const agents = loadAgents()
214
+ const agent = agents[agentId]
215
+ if (!agent) throw new Error(`Agent not found: ${agentId}`)
216
+
217
+ const agentDir = path.join(config.workspacePath, 'agents', agent.name.toLowerCase().replace(/\s+/g, '-'))
218
+ const updated: string[] = []
219
+
220
+ const soulPath = path.join(agentDir, 'SOUL.md')
221
+ if (fs.existsSync(soulPath)) {
222
+ agent.soul = fs.readFileSync(soulPath, 'utf8')
223
+ updated.push('soul')
224
+ }
225
+
226
+ const identityPath = path.join(agentDir, 'IDENTITY.md')
227
+ if (fs.existsSync(identityPath)) {
228
+ const content = fs.readFileSync(identityPath, 'utf8')
229
+ // Extract description: everything after the first heading and before the metadata lines
230
+ const lines = content.split('\n')
231
+ const descLines = lines.filter((l) => !l.startsWith('#') && !l.startsWith('- Provider:') && !l.startsWith('- Model:') && !l.startsWith('- Capabilities:'))
232
+ const desc = descLines.join('\n').trim()
233
+ if (desc) {
234
+ agent.description = desc
235
+ updated.push('description')
236
+ }
237
+ }
238
+
239
+ if (updated.length > 0) {
240
+ agent.updatedAt = Date.now()
241
+ agents[agentId] = agent
242
+ saveAgents(agents)
243
+ }
244
+
245
+ return { updated }
246
+ }
247
+
248
+ // --- Schedule Sync (Feature 6) ---
249
+
250
+ export function pushSchedulesToOpenClaw(): { written: number } {
251
+ const config = loadSyncConfig()
252
+ const cronDir = path.join(config.workspacePath, 'cron')
253
+ ensureDir(cronDir)
254
+
255
+ const schedules = loadSchedules()
256
+ const cronSchedules = Object.values(schedules).filter(
257
+ (s: any) => s.scheduleType === 'cron' && s.status === 'active',
258
+ )
259
+
260
+ const jobs = cronSchedules.map((s: any) => ({
261
+ name: s.name,
262
+ cron: s.cron,
263
+ agentId: s.agentId,
264
+ taskPrompt: s.taskPrompt,
265
+ status: s.status,
266
+ }))
267
+
268
+ fs.writeFileSync(path.join(cronDir, 'jobs.json'), JSON.stringify(jobs, null, 2))
269
+ return { written: jobs.length }
270
+ }
271
+
272
+ export function pullSchedulesFromOpenClaw(): { imported: number } {
273
+ const config = loadSyncConfig()
274
+ const jobsPath = path.join(config.workspacePath, 'cron', 'jobs.json')
275
+ if (!fs.existsSync(jobsPath)) return { imported: 0 }
276
+
277
+ const raw = JSON.parse(fs.readFileSync(jobsPath, 'utf8'))
278
+ if (!Array.isArray(raw)) return { imported: 0 }
279
+
280
+ const schedules = loadSchedules()
281
+ const existingNames = new Set(Object.values(schedules).map((s: any) => `${s.name}|${s.cron}`))
282
+ let imported = 0
283
+
284
+ for (const job of raw) {
285
+ if (!job.name || !job.cron) continue
286
+ const key = `${job.name}|${job.cron}`
287
+ if (existingNames.has(key)) continue
288
+
289
+ const id = crypto.randomUUID()
290
+ schedules[id] = {
291
+ id,
292
+ name: job.name,
293
+ agentId: job.agentId || '',
294
+ taskPrompt: job.taskPrompt || '',
295
+ scheduleType: 'cron',
296
+ cron: job.cron,
297
+ status: 'active',
298
+ createdAt: Date.now(),
299
+ }
300
+ existingNames.add(key)
301
+ imported++
302
+ }
303
+
304
+ if (imported > 0) saveSchedules(schedules)
305
+ return { imported }
306
+ }
307
+
308
+ // --- Secret/Credential Sync (Feature 7) ---
309
+
310
+ export function pullCredentialsFromOpenClaw(): { imported: number } {
311
+ const config = loadSyncConfig()
312
+ const modelsPath = path.join(config.workspacePath, 'agents', 'main', 'agent', 'models.json')
313
+ if (!fs.existsSync(modelsPath)) return { imported: 0 }
314
+
315
+ let raw: any
316
+ try {
317
+ raw = JSON.parse(fs.readFileSync(modelsPath, 'utf8'))
318
+ } catch {
319
+ return { imported: 0 }
320
+ }
321
+
322
+ const { loadCredentials: loadCreds, saveCredentials } = require('./storage')
323
+ const creds = loadCreds()
324
+ const existingProviders = new Set(Object.values(creds).map((c: any) => c.provider))
325
+ let imported = 0
326
+
327
+ // Extract API keys from models.json entries
328
+ const entries = Array.isArray(raw) ? raw : raw?.models ? raw.models : []
329
+ for (const entry of entries) {
330
+ if (!entry.apiKey || !entry.provider) continue
331
+ if (existingProviders.has(`openclaw-${entry.provider}`)) continue
332
+
333
+ const id = crypto.randomUUID()
334
+ creds[id] = {
335
+ id,
336
+ provider: `openclaw-${entry.provider}`,
337
+ name: `OpenClaw ${entry.provider}`,
338
+ encryptedKey: encryptKey(entry.apiKey),
339
+ createdAt: Date.now(),
340
+ }
341
+ existingProviders.add(`openclaw-${entry.provider}`)
342
+ imported++
343
+ }
344
+
345
+ if (imported > 0) saveCredentials(creds)
346
+ return { imported }
347
+ }
348
+
349
+ export function pushCredentialsToOpenClaw(): { written: boolean } {
350
+ const config = loadSyncConfig()
351
+ const authProfilesPath = path.join(config.workspacePath, 'auth-profiles.json')
352
+ const creds = loadCredentials()
353
+
354
+ const profiles: Record<string, string> = {}
355
+ for (const cred of Object.values(creds) as any[]) {
356
+ if (!cred.encryptedKey || !cred.provider) continue
357
+ try {
358
+ profiles[cred.provider] = decryptKey(cred.encryptedKey)
359
+ } catch { /* skip undecryptable */ }
360
+ }
361
+
362
+ if (Object.keys(profiles).length === 0) return { written: false }
363
+
364
+ ensureDir(path.dirname(authProfilesPath))
365
+ fs.writeFileSync(authProfilesPath, JSON.stringify(profiles, null, 2), { mode: 0o600 })
366
+ try { fs.chmodSync(authProfilesPath, 0o600) } catch { /* best effort */ }
367
+ return { written: true }
368
+ }
369
+
370
+ // --- Plugin Sync (Feature 11) ---
371
+
372
+ export function syncPluginsFromOpenClaw(): { imported: number } {
373
+ const config = loadSyncConfig()
374
+ const openclawPluginDir = path.join(config.workspacePath, 'plugins')
375
+ if (!fs.existsSync(openclawPluginDir)) return { imported: 0 }
376
+
377
+ const localPluginDir = path.join(DATA_DIR, 'plugins')
378
+ ensureDir(localPluginDir)
379
+
380
+ const files = fs.readdirSync(openclawPluginDir).filter((f) => f.endsWith('.js'))
381
+ const existingHashes = new Set<string>()
382
+ // Hash existing local plugins
383
+ if (fs.existsSync(localPluginDir)) {
384
+ for (const f of fs.readdirSync(localPluginDir).filter((f) => f.endsWith('.js'))) {
385
+ const content = fs.readFileSync(path.join(localPluginDir, f), 'utf8')
386
+ existingHashes.add(contentHash(content))
387
+ }
388
+ }
389
+
390
+ let imported = 0
391
+ for (const file of files) {
392
+ const content = fs.readFileSync(path.join(openclawPluginDir, file), 'utf8')
393
+ const hash = contentHash(content)
394
+ if (existingHashes.has(hash)) continue
395
+
396
+ const destName = `openclaw-${file}`
397
+ fs.writeFileSync(path.join(localPluginDir, destName), content)
398
+ existingHashes.add(hash)
399
+ imported++
400
+ }
401
+
402
+ return { imported }
403
+ }
404
+
405
+ // --- Device Token Cross-Sync (Feature 14) ---
406
+
407
+ const SHARED_TOKEN_PATH = path.join(DATA_DIR, 'openclaw', 'shared-device-token.json')
408
+
409
+ export function getSharedDeviceToken(): string | null {
410
+ try {
411
+ if (!fs.existsSync(SHARED_TOKEN_PATH)) return null
412
+ const raw = JSON.parse(fs.readFileSync(SHARED_TOKEN_PATH, 'utf8'))
413
+ return typeof raw?.token === 'string' && raw.token.trim() ? raw.token.trim() : null
414
+ } catch {
415
+ return null
416
+ }
417
+ }
418
+
419
+ export function setSharedDeviceToken(token: string): void {
420
+ const dir = path.dirname(SHARED_TOKEN_PATH)
421
+ ensureDir(dir)
422
+ fs.writeFileSync(SHARED_TOKEN_PATH, JSON.stringify({ token, updatedAt: Date.now() }, null, 2), { mode: 0o600 })
423
+ try { fs.chmodSync(SHARED_TOKEN_PATH, 0o600) } catch { /* best effort */ }
424
+ }
425
+
426
+ // --- Unified Sync Entry Point ---
427
+
428
+ export type SyncType = 'memory' | 'workspace' | 'schedules' | 'credentials' | 'plugins'
429
+
430
+ export interface SyncResult {
431
+ type: SyncType
432
+ action: 'push' | 'pull'
433
+ result: Record<string, unknown>
434
+ }
435
+
436
+ export async function runSync(params: {
437
+ action: 'push' | 'pull' | 'both'
438
+ types: SyncType[]
439
+ }): Promise<SyncResult[]> {
440
+ const results: SyncResult[] = []
441
+
442
+ for (const type of params.types) {
443
+ if (params.action === 'push' || params.action === 'both') {
444
+ switch (type) {
445
+ case 'memory':
446
+ results.push({ type, action: 'push', result: pushMemoryToOpenClaw() })
447
+ break
448
+ case 'workspace': {
449
+ const agents = loadAgents()
450
+ for (const id of Object.keys(agents)) {
451
+ try {
452
+ results.push({ type, action: 'push', result: { agentId: id, ...pushAgentToOpenClaw(id) } })
453
+ } catch { /* skip */ }
454
+ }
455
+ break
456
+ }
457
+ case 'schedules':
458
+ results.push({ type, action: 'push', result: pushSchedulesToOpenClaw() })
459
+ break
460
+ case 'credentials':
461
+ results.push({ type, action: 'push', result: pushCredentialsToOpenClaw() })
462
+ break
463
+ case 'plugins':
464
+ // Plugins only pull from OpenClaw
465
+ break
466
+ }
467
+ }
468
+ if (params.action === 'pull' || params.action === 'both') {
469
+ switch (type) {
470
+ case 'memory':
471
+ results.push({ type, action: 'pull', result: pullMemoryFromOpenClaw() })
472
+ break
473
+ case 'workspace': {
474
+ const agents = loadAgents()
475
+ for (const id of Object.keys(agents)) {
476
+ try {
477
+ results.push({ type, action: 'pull', result: { agentId: id, ...pullAgentFromOpenClaw(id) } })
478
+ } catch { /* skip */ }
479
+ }
480
+ break
481
+ }
482
+ case 'schedules':
483
+ results.push({ type, action: 'pull', result: pullSchedulesFromOpenClaw() })
484
+ break
485
+ case 'credentials':
486
+ results.push({ type, action: 'pull', result: pullCredentialsFromOpenClaw() })
487
+ break
488
+ case 'plugins':
489
+ results.push({ type, action: 'pull', result: syncPluginsFromOpenClaw() })
490
+ break
491
+ }
492
+ }
493
+ }
494
+
495
+ return results
496
+ }
@@ -11,11 +11,10 @@ import { buildChatModel } from './build-llm'
11
11
  import { getCheckpointSaver } from './langgraph-checkpoint'
12
12
  import { notify } from './ws-hub'
13
13
  import { pushMainLoopEventToMainSessions } from './main-agent-loop'
14
- import crypto from 'crypto'
14
+ import { genId } from '@/lib/id'
15
+ import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
15
16
  import type { Agent, TaskComment, MessageToolEvent } from '@/types'
16
17
 
17
- const NON_LANGGRAPH_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli'])
18
-
19
18
  function resolveCredential(credentialId: string | null | undefined): string | null {
20
19
  if (!credentialId) return null
21
20
  const creds = loadCredentials()
@@ -92,12 +91,11 @@ function saveMessage(sessionId: string, role: 'user' | 'assistant', text: string
92
91
  async function executeSubTaskViaCli(agent: Agent, task: string, parentSessionId: string): Promise<string> {
93
92
  // Dynamic import to avoid circular deps
94
93
  const { callProvider } = await import('./orchestrator')
95
- const crypto = await import('crypto')
96
94
  const { loadSessions: ls, saveSessions: ss } = await import('./storage')
97
95
 
98
96
  const sessions = ls()
99
97
  const parentSession = sessions[parentSessionId]
100
- const childId = crypto.randomBytes(4).toString('hex')
98
+ const childId = genId()
101
99
  sessions[childId] = {
102
100
  id: childId,
103
101
  name: `[Agent] ${agent.name}: ${task.slice(0, 40)}`,
@@ -273,7 +271,7 @@ export async function executeLangGraphOrchestrator(
273
271
  if (!t) return `Task "${taskId}" not found.`
274
272
  if (!t.comments) t.comments = []
275
273
  const c: TaskComment = {
276
- id: crypto.randomBytes(4).toString('hex'),
274
+ id: genId(),
277
275
  author: orchestrator.name,
278
276
  agentId: orchestrator.id,
279
277
  text: comment,
@@ -298,7 +296,7 @@ export async function executeLangGraphOrchestrator(
298
296
  const createTaskTool = tool(
299
297
  async ({ title, description: desc }) => {
300
298
  const tasks = loadTasks()
301
- const id = crypto.randomBytes(4).toString('hex')
299
+ const id = genId()
302
300
  tasks[id] = {
303
301
  id,
304
302
  title,
@@ -532,6 +530,29 @@ export async function executeLangGraphOrchestrator(
532
530
  const toolCalls = lastMsg?.tool_calls || lastMsg?.additional_kwargs?.tool_calls || []
533
531
  const pendingCall = toolCalls[0]
534
532
  if (pendingCall) {
533
+ // Try OpenClaw approval bridge first when agent uses openclaw provider
534
+ try {
535
+ if (orchestrator.provider === 'openclaw') {
536
+ const { forwardApprovalToOpenClaw } = await import('./openclaw-approvals')
537
+ const toolName = pendingCall.name || pendingCall.function?.name || 'unknown'
538
+ const toolArgs = pendingCall.args || (pendingCall.function?.arguments ? JSON.parse(pendingCall.function.arguments) : {})
539
+ const decision = await forwardApprovalToOpenClaw({ toolName, args: toolArgs })
540
+ if (decision) {
541
+ if (decision.approved) {
542
+ // OpenClaw approved — resume the graph instead of pausing
543
+ console.log(`[orchestrator-lg] OpenClaw approved tool "${toolName}" — resuming graph`)
544
+ // Don't set pendingApproval, let the loop continue
545
+ } else {
546
+ console.log(`[orchestrator-lg] OpenClaw rejected tool "${toolName}": ${decision.reason || 'no reason'}`)
547
+ // Fall through to SwarmClaw's pendingApproval UI
548
+ }
549
+ }
550
+ // If decision is null (socket unavailable), fall through to SwarmClaw UI
551
+ }
552
+ } catch {
553
+ // OpenClaw approval bridge not available — fall through
554
+ }
555
+
535
556
  const tasks = loadTasks()
536
557
  const t = tasks[taskId]
537
558
  if (t) {
@@ -668,7 +689,7 @@ export async function resumeLangGraphOrchestrator(
668
689
  if (!t) return `Task "${taskId}" not found.`
669
690
  if (!t.comments) t.comments = []
670
691
  t.comments.push({
671
- id: crypto.randomBytes(4).toString('hex'),
692
+ id: genId(),
672
693
  author: orchestrator.name,
673
694
  agentId: orchestrator.id,
674
695
  text: comment,
@@ -688,7 +709,7 @@ export async function resumeLangGraphOrchestrator(
688
709
  const createTaskTool = tool(
689
710
  async ({ title, description: desc }) => {
690
711
  const tasks = loadTasks()
691
- const id = crypto.randomBytes(4).toString('hex')
712
+ const id = genId()
692
713
  tasks[id] = {
693
714
  id, title, description: desc, status: 'backlog',
694
715
  agentId: orchestrator.id, sessionId: null, result: null, error: null,
@@ -1,4 +1,4 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import {
3
3
  loadSessions, saveSessions, loadAgents,
4
4
  loadCredentials, decryptKey, loadSettings, loadSkills,
@@ -20,7 +20,7 @@ export function createOrchestratorSession(
20
20
  cwd?: string,
21
21
  ): string {
22
22
  const sessions = loadSessions()
23
- const sessionId = crypto.randomBytes(4).toString('hex')
23
+ const sessionId = genId()
24
24
  sessions[sessionId] = {
25
25
  id: sessionId,
26
26
  name: `[Orch] ${orchestrator.name}: ${task.slice(0, 40)}`,
@@ -264,7 +264,7 @@ async function executeSubTask(
264
264
  // Look up parent session cwd to inherit
265
265
  const sessions = loadSessions()
266
266
  const parentSession = sessions[parentSessionId]
267
- const childId = crypto.randomBytes(4).toString('hex')
267
+ const childId = genId()
268
268
  const childSession = {
269
269
  id: childId,
270
270
  name: `[Agent] ${agent.name}: ${task.slice(0, 40)}`,
@@ -325,7 +325,7 @@ export async function callProvider(
325
325
 
326
326
  // Build a mock session for the provider
327
327
  const mockSession = {
328
- id: 'orch-' + crypto.randomBytes(2).toString('hex'),
328
+ id: 'orch-' + genId(2),
329
329
  provider: agent.provider,
330
330
  model: agent.model,
331
331
  credentialId: agent.credentialId,
@@ -1,4 +1,4 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import { spawn, type ChildProcessWithoutNullStreams } from 'child_process'
3
3
 
4
4
  const MAX_LOG_CHARS = 200_000
@@ -99,7 +99,7 @@ function getShellCommand(command: string): { shell: string; args: string[] } {
99
99
  }
100
100
 
101
101
  export async function startManagedProcess(opts: StartProcessOptions): Promise<StartProcessResult> {
102
- const id = crypto.randomBytes(8).toString('hex')
102
+ const id = genId(8)
103
103
  const timeoutMs = Math.max(1000, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS)
104
104
  const yieldMs = Math.max(250, opts.yieldMs ?? DEFAULT_BACKGROUND_YIELD_MS)
105
105
  const startedAt = now()