@swarmclawai/swarmclaw 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +21 -4
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +13 -0
  4. package/package.json +3 -1
  5. package/src/app/api/agents/[id]/route.ts +39 -22
  6. package/src/app/api/agents/[id]/thread/route.ts +2 -2
  7. package/src/app/api/agents/route.ts +3 -2
  8. package/src/app/api/agents/trash/route.ts +44 -0
  9. package/src/app/api/clawhub/install/route.ts +2 -2
  10. package/src/app/api/connectors/[id]/route.ts +17 -7
  11. package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
  12. package/src/app/api/connectors/route.ts +6 -3
  13. package/src/app/api/credentials/[id]/route.ts +2 -1
  14. package/src/app/api/credentials/route.ts +2 -2
  15. package/src/app/api/documents/route.ts +2 -2
  16. package/src/app/api/files/serve/route.ts +8 -0
  17. package/src/app/api/knowledge/[id]/route.ts +5 -4
  18. package/src/app/api/knowledge/upload/route.ts +2 -2
  19. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  20. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  21. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  22. package/src/app/api/mcp-servers/route.ts +2 -2
  23. package/src/app/api/memory/[id]/route.ts +9 -8
  24. package/src/app/api/memory/route.ts +2 -2
  25. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  26. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  27. package/src/app/api/openclaw/approvals/route.ts +46 -0
  28. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  29. package/src/app/api/openclaw/cron/route.ts +52 -0
  30. package/src/app/api/openclaw/directory/route.ts +27 -0
  31. package/src/app/api/openclaw/discover/route.ts +62 -0
  32. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  33. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  34. package/src/app/api/openclaw/gateway/route.ts +72 -0
  35. package/src/app/api/openclaw/history/route.ts +109 -0
  36. package/src/app/api/openclaw/media/route.ts +53 -0
  37. package/src/app/api/openclaw/models/route.ts +12 -0
  38. package/src/app/api/openclaw/permissions/route.ts +39 -0
  39. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  40. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  41. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  42. package/src/app/api/openclaw/skills/route.ts +82 -0
  43. package/src/app/api/openclaw/sync/route.ts +31 -0
  44. package/src/app/api/orchestrator/run/route.ts +2 -2
  45. package/src/app/api/projects/[id]/route.ts +55 -0
  46. package/src/app/api/projects/route.ts +27 -0
  47. package/src/app/api/providers/[id]/models/route.ts +2 -1
  48. package/src/app/api/providers/[id]/route.ts +13 -15
  49. package/src/app/api/providers/route.ts +2 -2
  50. package/src/app/api/schedules/[id]/route.ts +16 -18
  51. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  52. package/src/app/api/schedules/route.ts +2 -2
  53. package/src/app/api/secrets/[id]/route.ts +16 -17
  54. package/src/app/api/secrets/route.ts +2 -2
  55. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  56. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  57. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  58. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  59. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  60. package/src/app/api/sessions/[id]/messages/route.ts +20 -2
  61. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  62. package/src/app/api/sessions/[id]/route.ts +14 -4
  63. package/src/app/api/sessions/route.ts +8 -4
  64. package/src/app/api/skills/[id]/route.ts +23 -21
  65. package/src/app/api/skills/import/route.ts +2 -2
  66. package/src/app/api/skills/route.ts +2 -2
  67. package/src/app/api/tasks/[id]/approve/route.ts +2 -1
  68. package/src/app/api/tasks/[id]/route.ts +6 -5
  69. package/src/app/api/tasks/route.ts +2 -2
  70. package/src/app/api/tts/stream/route.ts +48 -0
  71. package/src/app/api/upload/route.ts +2 -2
  72. package/src/app/api/uploads/[filename]/route.ts +4 -1
  73. package/src/app/api/webhooks/[id]/route.ts +29 -31
  74. package/src/app/api/webhooks/route.ts +2 -2
  75. package/src/app/globals.css +14 -0
  76. package/src/app/layout.tsx +5 -20
  77. package/src/app/page.tsx +3 -24
  78. package/src/cli/index.js +60 -0
  79. package/src/cli/index.ts +1 -1
  80. package/src/cli/spec.js +42 -0
  81. package/src/components/agents/agent-avatar.tsx +45 -0
  82. package/src/components/agents/agent-card.tsx +19 -5
  83. package/src/components/agents/agent-chat-list.tsx +31 -24
  84. package/src/components/agents/agent-files-editor.tsx +185 -0
  85. package/src/components/agents/agent-list.tsx +84 -3
  86. package/src/components/agents/agent-sheet.tsx +147 -14
  87. package/src/components/agents/cron-job-form.tsx +137 -0
  88. package/src/components/agents/exec-config-panel.tsx +147 -0
  89. package/src/components/agents/inspector-panel.tsx +310 -0
  90. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  91. package/src/components/agents/permission-preset-selector.tsx +79 -0
  92. package/src/components/agents/personality-builder.tsx +111 -0
  93. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  94. package/src/components/agents/skill-install-dialog.tsx +102 -0
  95. package/src/components/agents/trash-list.tsx +109 -0
  96. package/src/components/chat/chat-area.tsx +41 -6
  97. package/src/components/chat/chat-header.tsx +305 -29
  98. package/src/components/chat/chat-preview-panel.tsx +113 -0
  99. package/src/components/chat/exec-approval-card.tsx +89 -0
  100. package/src/components/chat/message-bubble.tsx +218 -36
  101. package/src/components/chat/message-list.tsx +135 -31
  102. package/src/components/chat/streaming-bubble.tsx +59 -10
  103. package/src/components/chat/suggestions-bar.tsx +74 -0
  104. package/src/components/chat/thinking-indicator.tsx +20 -6
  105. package/src/components/chat/tool-call-bubble.tsx +98 -19
  106. package/src/components/chat/tool-request-banner.tsx +20 -2
  107. package/src/components/chat/trace-block.tsx +103 -0
  108. package/src/components/chat/voice-overlay.tsx +80 -0
  109. package/src/components/connectors/connector-list.tsx +6 -2
  110. package/src/components/connectors/connector-sheet.tsx +31 -7
  111. package/src/components/layout/app-layout.tsx +47 -25
  112. package/src/components/projects/project-list.tsx +123 -0
  113. package/src/components/projects/project-sheet.tsx +135 -0
  114. package/src/components/schedules/schedule-list.tsx +3 -1
  115. package/src/components/sessions/new-session-sheet.tsx +6 -6
  116. package/src/components/sessions/session-card.tsx +1 -1
  117. package/src/components/sessions/session-list.tsx +7 -7
  118. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  119. package/src/components/shared/avatar.tsx +13 -2
  120. package/src/components/shared/connector-platform-icon.tsx +4 -0
  121. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  122. package/src/components/shared/settings/section-orchestrator.tsx +1 -2
  123. package/src/components/shared/settings/section-web-search.tsx +56 -0
  124. package/src/components/shared/settings/settings-page.tsx +74 -0
  125. package/src/components/skills/skill-list.tsx +2 -1
  126. package/src/components/tasks/task-board.tsx +1 -1
  127. package/src/components/tasks/task-list.tsx +5 -2
  128. package/src/components/tasks/task-sheet.tsx +12 -12
  129. package/src/hooks/use-continuous-speech.ts +181 -0
  130. package/src/hooks/use-openclaw-gateway.ts +63 -0
  131. package/src/hooks/use-view-router.ts +52 -0
  132. package/src/hooks/use-voice-conversation.ts +80 -0
  133. package/src/lib/id.ts +6 -0
  134. package/src/lib/notification-sounds.ts +58 -0
  135. package/src/lib/personality-parser.ts +97 -0
  136. package/src/lib/projects.ts +13 -0
  137. package/src/lib/provider-sets.ts +5 -0
  138. package/src/lib/providers/anthropic.ts +14 -1
  139. package/src/lib/providers/index.ts +6 -0
  140. package/src/lib/providers/ollama.ts +9 -1
  141. package/src/lib/providers/openai.ts +9 -1
  142. package/src/lib/providers/openclaw.ts +28 -2
  143. package/src/lib/runtime-loop.ts +2 -2
  144. package/src/lib/server/api-routes.test.ts +5 -6
  145. package/src/lib/server/build-llm.ts +17 -4
  146. package/src/lib/server/chat-execution.ts +82 -6
  147. package/src/lib/server/collection-helpers.ts +54 -0
  148. package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
  149. package/src/lib/server/connectors/bluebubbles.ts +360 -0
  150. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  151. package/src/lib/server/connectors/googlechat.ts +51 -8
  152. package/src/lib/server/connectors/manager.ts +424 -13
  153. package/src/lib/server/connectors/media.ts +2 -2
  154. package/src/lib/server/connectors/openclaw.ts +65 -0
  155. package/src/lib/server/connectors/pairing.test.ts +99 -0
  156. package/src/lib/server/connectors/pairing.ts +256 -0
  157. package/src/lib/server/connectors/signal.ts +1 -0
  158. package/src/lib/server/connectors/teams.ts +5 -5
  159. package/src/lib/server/connectors/types.ts +10 -0
  160. package/src/lib/server/daemon-state.ts +11 -0
  161. package/src/lib/server/execution-log.ts +3 -3
  162. package/src/lib/server/heartbeat-service.ts +1 -1
  163. package/src/lib/server/knowledge-db.test.ts +2 -33
  164. package/src/lib/server/main-agent-loop.ts +8 -9
  165. package/src/lib/server/main-session.ts +21 -0
  166. package/src/lib/server/memory-db.ts +6 -6
  167. package/src/lib/server/openclaw-approvals.ts +105 -0
  168. package/src/lib/server/openclaw-config-sync.ts +107 -0
  169. package/src/lib/server/openclaw-exec-config.ts +52 -0
  170. package/src/lib/server/openclaw-gateway.ts +291 -0
  171. package/src/lib/server/openclaw-history-merge.ts +36 -0
  172. package/src/lib/server/openclaw-models.ts +56 -0
  173. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  174. package/src/lib/server/openclaw-sync.ts +497 -0
  175. package/src/lib/server/orchestrator-lg.ts +30 -9
  176. package/src/lib/server/orchestrator.ts +4 -4
  177. package/src/lib/server/process-manager.ts +2 -2
  178. package/src/lib/server/queue.ts +24 -11
  179. package/src/lib/server/scheduler.ts +2 -2
  180. package/src/lib/server/session-mailbox.ts +2 -2
  181. package/src/lib/server/session-run-manager.ts +2 -2
  182. package/src/lib/server/session-tools/connector.ts +53 -6
  183. package/src/lib/server/session-tools/crud.ts +3 -3
  184. package/src/lib/server/session-tools/delegate.ts +22 -6
  185. package/src/lib/server/session-tools/file.ts +192 -19
  186. package/src/lib/server/session-tools/index.ts +4 -2
  187. package/src/lib/server/session-tools/memory.ts +2 -2
  188. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  189. package/src/lib/server/session-tools/sandbox.ts +33 -0
  190. package/src/lib/server/session-tools/search-providers.ts +277 -0
  191. package/src/lib/server/session-tools/session-info.ts +2 -2
  192. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  193. package/src/lib/server/session-tools/shell.ts +1 -1
  194. package/src/lib/server/session-tools/web.ts +53 -72
  195. package/src/lib/server/storage.ts +74 -11
  196. package/src/lib/server/stream-agent-chat.ts +53 -4
  197. package/src/lib/server/suggestions.ts +20 -0
  198. package/src/lib/server/task-result.test.ts +44 -0
  199. package/src/lib/server/task-result.ts +14 -0
  200. package/src/lib/server/ws-hub.ts +14 -0
  201. package/src/lib/tool-definitions.ts +5 -3
  202. package/src/lib/tts-stream.ts +130 -0
  203. package/src/lib/view-routes.ts +28 -0
  204. package/src/proxy.ts +3 -0
  205. package/src/stores/use-app-store.ts +80 -1
  206. package/src/stores/use-approval-store.ts +78 -0
  207. package/src/stores/use-chat-store.ts +162 -6
  208. package/src/types/index.ts +154 -3
  209. package/tsconfig.json +13 -4
@@ -0,0 +1,64 @@
1
+ import type { ExecApprovalConfig, PermissionPreset } from '@/types'
2
+ import { ensureGatewayConnected } from './openclaw-gateway'
3
+ import { setExecConfig, getExecConfig } from './openclaw-exec-config'
4
+
5
+ export interface PresetConfig {
6
+ security: ExecApprovalConfig['security']
7
+ askMode: ExecApprovalConfig['askMode']
8
+ toolGroups: string[]
9
+ }
10
+
11
+ export const PRESET_CONFIGS: Record<PermissionPreset, PresetConfig> = {
12
+ conservative: {
13
+ security: 'deny',
14
+ askMode: 'off',
15
+ toolGroups: [],
16
+ },
17
+ collaborative: {
18
+ security: 'allowlist',
19
+ askMode: 'on-miss',
20
+ toolGroups: ['group:web', 'group:fs'],
21
+ },
22
+ autonomous: {
23
+ security: 'full',
24
+ askMode: 'off',
25
+ toolGroups: ['group:runtime', 'group:web', 'group:fs'],
26
+ },
27
+ }
28
+
29
+ /** Derive which preset matches the current config, or 'custom' if none match */
30
+ export function resolvePresetFromConfig(config: ExecApprovalConfig): PermissionPreset | 'custom' {
31
+ for (const [preset, pc] of Object.entries(PRESET_CONFIGS) as [PermissionPreset, PresetConfig][]) {
32
+ if (config.security === pc.security && config.askMode === pc.askMode) {
33
+ return preset
34
+ }
35
+ }
36
+ return 'custom'
37
+ }
38
+
39
+ /** Apply a permission preset to an agent via gateway RPC */
40
+ export async function applyPreset(agentId: string, preset: PermissionPreset): Promise<void> {
41
+ const pc = PRESET_CONFIGS[preset]
42
+ if (!pc) throw new Error(`Unknown preset: ${preset}`)
43
+
44
+ const gw = await ensureGatewayConnected()
45
+ if (!gw) throw new Error('Gateway not connected')
46
+
47
+ // Update exec approval config
48
+ const snap = await getExecConfig(agentId)
49
+ await setExecConfig(agentId, {
50
+ security: pc.security,
51
+ askMode: pc.askMode,
52
+ patterns: pc.security === 'allowlist' ? snap.file.patterns : [],
53
+ }, snap.hash)
54
+
55
+ // Sync tool groups if gateway supports it
56
+ try {
57
+ await gw.rpc('config.set', {
58
+ key: `agents.${agentId}.toolGroups`,
59
+ value: pc.toolGroups,
60
+ })
61
+ } catch {
62
+ // Not all gateways support tool group config — ignore
63
+ }
64
+ }
@@ -0,0 +1,497 @@
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() as Record<string, Schedule>
256
+ const cronSchedules = Object.values(schedules).filter(
257
+ (s) => s.scheduleType === 'cron' && s.status === 'active',
258
+ )
259
+
260
+ const jobs = cronSchedules.map((s) => ({
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() as Record<string, Schedule>
281
+ const existingNames = new Set(Object.values(schedules).map((s) => `${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 async function pullCredentialsFromOpenClaw(): Promise<{ 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
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
316
+ let raw: any
317
+ try {
318
+ raw = JSON.parse(fs.readFileSync(modelsPath, 'utf8'))
319
+ } catch {
320
+ return { imported: 0 }
321
+ }
322
+
323
+ const { loadCredentials: loadCreds, saveCredentials } = await import('./storage')
324
+ const creds = loadCreds()
325
+ const existingProviders = new Set(Object.values(creds).map((c: Record<string, unknown>) => c.provider))
326
+ let imported = 0
327
+
328
+ // Extract API keys from models.json entries
329
+ const entries = Array.isArray(raw) ? raw : raw?.models ? raw.models : []
330
+ for (const entry of entries) {
331
+ if (!entry.apiKey || !entry.provider) continue
332
+ if (existingProviders.has(`openclaw-${entry.provider}`)) continue
333
+
334
+ const id = crypto.randomUUID()
335
+ creds[id] = {
336
+ id,
337
+ provider: `openclaw-${entry.provider}`,
338
+ name: `OpenClaw ${entry.provider}`,
339
+ encryptedKey: encryptKey(entry.apiKey),
340
+ createdAt: Date.now(),
341
+ }
342
+ existingProviders.add(`openclaw-${entry.provider}`)
343
+ imported++
344
+ }
345
+
346
+ if (imported > 0) saveCredentials(creds)
347
+ return { imported }
348
+ }
349
+
350
+ export function pushCredentialsToOpenClaw(): { written: boolean } {
351
+ const config = loadSyncConfig()
352
+ const authProfilesPath = path.join(config.workspacePath, 'auth-profiles.json')
353
+ const creds = loadCredentials()
354
+
355
+ const profiles: Record<string, string> = {}
356
+ for (const cred of Object.values(creds) as Array<Record<string, string>>) {
357
+ if (!cred.encryptedKey || !cred.provider) continue
358
+ try {
359
+ profiles[cred.provider] = decryptKey(cred.encryptedKey)
360
+ } catch { /* skip undecryptable */ }
361
+ }
362
+
363
+ if (Object.keys(profiles).length === 0) return { written: false }
364
+
365
+ ensureDir(path.dirname(authProfilesPath))
366
+ fs.writeFileSync(authProfilesPath, JSON.stringify(profiles, null, 2), { mode: 0o600 })
367
+ try { fs.chmodSync(authProfilesPath, 0o600) } catch { /* best effort */ }
368
+ return { written: true }
369
+ }
370
+
371
+ // --- Plugin Sync (Feature 11) ---
372
+
373
+ export function syncPluginsFromOpenClaw(): { imported: number } {
374
+ const config = loadSyncConfig()
375
+ const openclawPluginDir = path.join(config.workspacePath, 'plugins')
376
+ if (!fs.existsSync(openclawPluginDir)) return { imported: 0 }
377
+
378
+ const localPluginDir = path.join(DATA_DIR, 'plugins')
379
+ ensureDir(localPluginDir)
380
+
381
+ const files = fs.readdirSync(openclawPluginDir).filter((f) => f.endsWith('.js'))
382
+ const existingHashes = new Set<string>()
383
+ // Hash existing local plugins
384
+ if (fs.existsSync(localPluginDir)) {
385
+ for (const f of fs.readdirSync(localPluginDir).filter((f) => f.endsWith('.js'))) {
386
+ const content = fs.readFileSync(path.join(localPluginDir, f), 'utf8')
387
+ existingHashes.add(contentHash(content))
388
+ }
389
+ }
390
+
391
+ let imported = 0
392
+ for (const file of files) {
393
+ const content = fs.readFileSync(path.join(openclawPluginDir, file), 'utf8')
394
+ const hash = contentHash(content)
395
+ if (existingHashes.has(hash)) continue
396
+
397
+ const destName = `openclaw-${file}`
398
+ fs.writeFileSync(path.join(localPluginDir, destName), content)
399
+ existingHashes.add(hash)
400
+ imported++
401
+ }
402
+
403
+ return { imported }
404
+ }
405
+
406
+ // --- Device Token Cross-Sync (Feature 14) ---
407
+
408
+ const SHARED_TOKEN_PATH = path.join(DATA_DIR, 'openclaw', 'shared-device-token.json')
409
+
410
+ export function getSharedDeviceToken(): string | null {
411
+ try {
412
+ if (!fs.existsSync(SHARED_TOKEN_PATH)) return null
413
+ const raw = JSON.parse(fs.readFileSync(SHARED_TOKEN_PATH, 'utf8'))
414
+ return typeof raw?.token === 'string' && raw.token.trim() ? raw.token.trim() : null
415
+ } catch {
416
+ return null
417
+ }
418
+ }
419
+
420
+ export function setSharedDeviceToken(token: string): void {
421
+ const dir = path.dirname(SHARED_TOKEN_PATH)
422
+ ensureDir(dir)
423
+ fs.writeFileSync(SHARED_TOKEN_PATH, JSON.stringify({ token, updatedAt: Date.now() }, null, 2), { mode: 0o600 })
424
+ try { fs.chmodSync(SHARED_TOKEN_PATH, 0o600) } catch { /* best effort */ }
425
+ }
426
+
427
+ // --- Unified Sync Entry Point ---
428
+
429
+ export type SyncType = 'memory' | 'workspace' | 'schedules' | 'credentials' | 'plugins'
430
+
431
+ export interface SyncResult {
432
+ type: SyncType
433
+ action: 'push' | 'pull'
434
+ result: Record<string, unknown>
435
+ }
436
+
437
+ export async function runSync(params: {
438
+ action: 'push' | 'pull' | 'both'
439
+ types: SyncType[]
440
+ }): Promise<SyncResult[]> {
441
+ const results: SyncResult[] = []
442
+
443
+ for (const type of params.types) {
444
+ if (params.action === 'push' || params.action === 'both') {
445
+ switch (type) {
446
+ case 'memory':
447
+ results.push({ type, action: 'push', result: pushMemoryToOpenClaw() })
448
+ break
449
+ case 'workspace': {
450
+ const agents = loadAgents()
451
+ for (const id of Object.keys(agents)) {
452
+ try {
453
+ results.push({ type, action: 'push', result: { agentId: id, ...pushAgentToOpenClaw(id) } })
454
+ } catch { /* skip */ }
455
+ }
456
+ break
457
+ }
458
+ case 'schedules':
459
+ results.push({ type, action: 'push', result: pushSchedulesToOpenClaw() })
460
+ break
461
+ case 'credentials':
462
+ results.push({ type, action: 'push', result: pushCredentialsToOpenClaw() })
463
+ break
464
+ case 'plugins':
465
+ // Plugins only pull from OpenClaw
466
+ break
467
+ }
468
+ }
469
+ if (params.action === 'pull' || params.action === 'both') {
470
+ switch (type) {
471
+ case 'memory':
472
+ results.push({ type, action: 'pull', result: pullMemoryFromOpenClaw() })
473
+ break
474
+ case 'workspace': {
475
+ const agents = loadAgents()
476
+ for (const id of Object.keys(agents)) {
477
+ try {
478
+ results.push({ type, action: 'pull', result: { agentId: id, ...pullAgentFromOpenClaw(id) } })
479
+ } catch { /* skip */ }
480
+ }
481
+ break
482
+ }
483
+ case 'schedules':
484
+ results.push({ type, action: 'pull', result: pullSchedulesFromOpenClaw() })
485
+ break
486
+ case 'credentials':
487
+ results.push({ type, action: 'pull', result: await pullCredentialsFromOpenClaw() })
488
+ break
489
+ case 'plugins':
490
+ results.push({ type, action: 'pull', result: syncPluginsFromOpenClaw() })
491
+ break
492
+ }
493
+ }
494
+ }
495
+
496
+ return results
497
+ }
@@ -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,