@swarmclawai/swarmclaw 0.3.1 → 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 (203) hide show
  1. package/README.md +33 -13
  2. package/bin/server-cmd.js +14 -7
  3. package/bin/swarmclaw.js +3 -1
  4. package/bin/update-cmd.js +120 -0
  5. package/next.config.ts +10 -0
  6. package/package.json +4 -1
  7. package/src/app/api/agents/[id]/route.ts +20 -18
  8. package/src/app/api/agents/[id]/thread/route.ts +4 -3
  9. package/src/app/api/agents/route.ts +8 -3
  10. package/src/app/api/auth/route.ts +3 -1
  11. package/src/app/api/claude-skills/route.ts +3 -1
  12. package/src/app/api/clawhub/install/route.ts +2 -2
  13. package/src/app/api/connectors/[id]/route.ts +14 -3
  14. package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
  15. package/src/app/api/connectors/route.ts +12 -4
  16. package/src/app/api/credentials/[id]/route.ts +2 -1
  17. package/src/app/api/credentials/route.ts +5 -3
  18. package/src/app/api/daemon/route.ts +6 -1
  19. package/src/app/api/documents/route.ts +2 -2
  20. package/src/app/api/files/serve/route.ts +8 -0
  21. package/src/app/api/ip/route.ts +3 -1
  22. package/src/app/api/knowledge/[id]/route.ts +5 -4
  23. package/src/app/api/knowledge/upload/route.ts +2 -2
  24. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  25. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  26. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  27. package/src/app/api/mcp-servers/route.ts +5 -3
  28. package/src/app/api/memory/[id]/route.ts +9 -8
  29. package/src/app/api/memory/route.ts +2 -2
  30. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  31. package/src/app/api/openclaw/directory/route.ts +26 -0
  32. package/src/app/api/openclaw/discover/route.ts +61 -0
  33. package/src/app/api/openclaw/sync/route.ts +30 -0
  34. package/src/app/api/orchestrator/graph/route.ts +25 -0
  35. package/src/app/api/orchestrator/run/route.ts +2 -2
  36. package/src/app/api/plugins/marketplace/route.ts +3 -1
  37. package/src/app/api/plugins/route.ts +3 -1
  38. package/src/app/api/projects/[id]/route.ts +55 -0
  39. package/src/app/api/projects/route.ts +27 -0
  40. package/src/app/api/providers/[id]/models/route.ts +2 -1
  41. package/src/app/api/providers/[id]/route.ts +13 -12
  42. package/src/app/api/providers/configs/route.ts +3 -1
  43. package/src/app/api/providers/route.ts +7 -3
  44. package/src/app/api/schedules/[id]/route.ts +16 -15
  45. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  46. package/src/app/api/schedules/route.ts +8 -3
  47. package/src/app/api/secrets/[id]/route.ts +16 -17
  48. package/src/app/api/secrets/route.ts +5 -3
  49. package/src/app/api/sessions/[id]/chat/route.ts +5 -2
  50. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  51. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  52. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  53. package/src/app/api/sessions/[id]/messages/route.ts +2 -1
  54. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  55. package/src/app/api/sessions/[id]/route.ts +2 -1
  56. package/src/app/api/sessions/route.ts +11 -4
  57. package/src/app/api/settings/route.ts +3 -1
  58. package/src/app/api/setup/doctor/route.ts +1 -0
  59. package/src/app/api/setup/openclaw-device/route.ts +3 -1
  60. package/src/app/api/skills/[id]/route.ts +23 -21
  61. package/src/app/api/skills/import/route.ts +2 -2
  62. package/src/app/api/skills/route.ts +5 -3
  63. package/src/app/api/tasks/[id]/approve/route.ts +74 -0
  64. package/src/app/api/tasks/[id]/route.ts +9 -5
  65. package/src/app/api/tasks/route.ts +5 -2
  66. package/src/app/api/tts/stream/route.ts +48 -0
  67. package/src/app/api/upload/route.ts +2 -2
  68. package/src/app/api/uploads/[filename]/route.ts +4 -1
  69. package/src/app/api/usage/route.ts +3 -1
  70. package/src/app/api/version/route.ts +3 -1
  71. package/src/app/api/webhooks/[id]/route.ts +31 -32
  72. package/src/app/api/webhooks/route.ts +5 -3
  73. package/src/app/icon.svg +58 -0
  74. package/src/app/page.tsx +11 -26
  75. package/src/cli/index.js +28 -9
  76. package/src/cli/index.ts +45 -2
  77. package/src/cli/spec.js +2 -8
  78. package/src/components/agents/agent-card.tsx +1 -1
  79. package/src/components/agents/agent-list.tsx +3 -1
  80. package/src/components/agents/agent-sheet.tsx +166 -81
  81. package/src/components/chat/chat-area.tsx +71 -34
  82. package/src/components/chat/chat-header.tsx +141 -29
  83. package/src/components/chat/chat-tool-toggles.tsx +12 -53
  84. package/src/components/chat/message-bubble.tsx +110 -42
  85. package/src/components/chat/tool-call-bubble.tsx +50 -6
  86. package/src/components/chat/tool-request-banner.tsx +1 -9
  87. package/src/components/chat/voice-overlay.tsx +80 -0
  88. package/src/components/connectors/connector-list.tsx +9 -10
  89. package/src/components/connectors/connector-sheet.tsx +55 -36
  90. package/src/components/input/chat-input.tsx +72 -56
  91. package/src/components/knowledge/knowledge-list.tsx +27 -31
  92. package/src/components/layout/app-layout.tsx +133 -90
  93. package/src/components/layout/daemon-indicator.tsx +3 -5
  94. package/src/components/logs/log-list.tsx +5 -9
  95. package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
  96. package/src/components/memory/memory-detail.tsx +1 -1
  97. package/src/components/plugins/plugin-list.tsx +227 -27
  98. package/src/components/projects/project-list.tsx +122 -0
  99. package/src/components/projects/project-sheet.tsx +135 -0
  100. package/src/components/providers/provider-list.tsx +46 -13
  101. package/src/components/providers/provider-sheet.tsx +0 -45
  102. package/src/components/runs/run-list.tsx +6 -15
  103. package/src/components/schedules/schedule-card.tsx +54 -4
  104. package/src/components/schedules/schedule-list.tsx +9 -4
  105. package/src/components/schedules/schedule-sheet.tsx +0 -47
  106. package/src/components/secrets/secrets-list.tsx +20 -2
  107. package/src/components/sessions/new-session-sheet.tsx +14 -15
  108. package/src/components/sessions/session-card.tsx +1 -1
  109. package/src/components/sessions/session-list.tsx +7 -7
  110. package/src/components/shared/connector-platform-icon.tsx +26 -20
  111. package/src/components/shared/model-combobox.tsx +148 -0
  112. package/src/components/shared/settings/section-heartbeat.tsx +8 -40
  113. package/src/components/shared/settings/section-orchestrator.tsx +9 -11
  114. package/src/components/shared/settings/section-web-search.tsx +56 -0
  115. package/src/components/shared/settings/settings-page.tsx +73 -0
  116. package/src/components/skills/skill-list.tsx +262 -35
  117. package/src/components/skills/skill-sheet.tsx +0 -45
  118. package/src/components/tasks/task-board.tsx +3 -6
  119. package/src/components/tasks/task-card.tsx +43 -1
  120. package/src/components/tasks/task-list.tsx +8 -7
  121. package/src/components/tasks/task-sheet.tsx +0 -44
  122. package/src/components/usage/usage-list.tsx +12 -4
  123. package/src/hooks/use-continuous-speech.ts +144 -0
  124. package/src/hooks/use-view-router.ts +52 -0
  125. package/src/hooks/use-voice-conversation.ts +80 -0
  126. package/src/hooks/use-ws.ts +66 -0
  127. package/src/instrumentation.ts +2 -0
  128. package/src/lib/chat.ts +14 -2
  129. package/src/lib/id.ts +6 -0
  130. package/src/lib/projects.ts +13 -0
  131. package/src/lib/provider-sets.ts +5 -0
  132. package/src/lib/providers/anthropic.ts +15 -2
  133. package/src/lib/providers/index.ts +8 -0
  134. package/src/lib/providers/ollama.ts +10 -2
  135. package/src/lib/providers/openai.ts +42 -13
  136. package/src/lib/providers/openclaw.ts +11 -0
  137. package/src/lib/server/api-routes.test.ts +5 -6
  138. package/src/lib/server/build-llm.ts +17 -4
  139. package/src/lib/server/chat-execution.ts +57 -8
  140. package/src/lib/server/collection-helpers.ts +54 -0
  141. package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
  142. package/src/lib/server/connectors/bluebubbles.ts +357 -0
  143. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  144. package/src/lib/server/connectors/googlechat.ts +46 -7
  145. package/src/lib/server/connectors/manager.ts +401 -6
  146. package/src/lib/server/connectors/media.ts +2 -2
  147. package/src/lib/server/connectors/openclaw.ts +64 -0
  148. package/src/lib/server/connectors/pairing.test.ts +99 -0
  149. package/src/lib/server/connectors/pairing.ts +256 -0
  150. package/src/lib/server/connectors/signal.ts +1 -0
  151. package/src/lib/server/connectors/teams.ts +5 -5
  152. package/src/lib/server/connectors/types.ts +10 -0
  153. package/src/lib/server/context-manager.ts +1 -1
  154. package/src/lib/server/daemon-state.ts +3 -0
  155. package/src/lib/server/data-dir.ts +1 -0
  156. package/src/lib/server/execution-log.ts +3 -3
  157. package/src/lib/server/heartbeat-service.ts +67 -3
  158. package/src/lib/server/knowledge-db.test.ts +2 -33
  159. package/src/lib/server/langgraph-checkpoint.ts +274 -0
  160. package/src/lib/server/main-agent-loop.ts +67 -8
  161. package/src/lib/server/memory-db.ts +6 -6
  162. package/src/lib/server/openclaw-approvals.ts +105 -0
  163. package/src/lib/server/openclaw-sync.ts +496 -0
  164. package/src/lib/server/orchestrator-lg.ts +422 -20
  165. package/src/lib/server/orchestrator.ts +29 -9
  166. package/src/lib/server/process-manager.ts +2 -2
  167. package/src/lib/server/queue.ts +39 -13
  168. package/src/lib/server/scheduler.ts +2 -2
  169. package/src/lib/server/session-mailbox.ts +2 -2
  170. package/src/lib/server/session-run-manager.ts +8 -3
  171. package/src/lib/server/session-tools/connector.ts +51 -4
  172. package/src/lib/server/session-tools/crud.ts +3 -3
  173. package/src/lib/server/session-tools/delegate.ts +5 -5
  174. package/src/lib/server/session-tools/file.ts +176 -3
  175. package/src/lib/server/session-tools/index.ts +4 -0
  176. package/src/lib/server/session-tools/memory.ts +2 -2
  177. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  178. package/src/lib/server/session-tools/sandbox.ts +197 -0
  179. package/src/lib/server/session-tools/search-providers.ts +270 -0
  180. package/src/lib/server/session-tools/session-info.ts +2 -2
  181. package/src/lib/server/session-tools/web.ts +47 -66
  182. package/src/lib/server/storage-mcp.test.ts +25 -2
  183. package/src/lib/server/storage.ts +36 -7
  184. package/src/lib/server/stream-agent-chat.ts +106 -22
  185. package/src/lib/server/task-result.test.ts +44 -0
  186. package/src/lib/server/task-result.ts +14 -0
  187. package/src/lib/server/task-validation.test.ts +23 -0
  188. package/src/lib/server/task-validation.ts +5 -3
  189. package/src/lib/server/ws-hub.ts +85 -0
  190. package/src/lib/tool-definitions.ts +44 -0
  191. package/src/lib/tts-stream.ts +130 -0
  192. package/src/lib/upload.ts +7 -1
  193. package/src/lib/view-routes.ts +28 -0
  194. package/src/lib/ws-client.ts +124 -0
  195. package/src/proxy.ts +3 -0
  196. package/src/stores/use-app-store.ts +28 -1
  197. package/src/stores/use-chat-store.ts +42 -14
  198. package/src/types/index.ts +34 -2
  199. package/src/app/api/agents/generate/route.ts +0 -42
  200. package/src/app/api/generate/info/route.ts +0 -12
  201. package/src/app/api/generate/route.ts +0 -106
  202. package/src/app/favicon.ico +0 -0
  203. package/src/components/shared/ai-gen-block.tsx +0 -77
@@ -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
+ }