@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.
- package/README.md +33 -13
- package/bin/server-cmd.js +14 -7
- package/bin/swarmclaw.js +3 -1
- package/bin/update-cmd.js +120 -0
- package/next.config.ts +10 -0
- package/package.json +4 -1
- package/src/app/api/agents/[id]/route.ts +20 -18
- package/src/app/api/agents/[id]/thread/route.ts +4 -3
- package/src/app/api/agents/route.ts +8 -3
- package/src/app/api/auth/route.ts +3 -1
- package/src/app/api/claude-skills/route.ts +3 -1
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +14 -3
- package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
- package/src/app/api/connectors/route.ts +12 -4
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +5 -3
- package/src/app/api/daemon/route.ts +6 -1
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/ip/route.ts +3 -1
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +5 -3
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- package/src/app/api/openclaw/directory/route.ts +26 -0
- package/src/app/api/openclaw/discover/route.ts +61 -0
- package/src/app/api/openclaw/sync/route.ts +30 -0
- package/src/app/api/orchestrator/graph/route.ts +25 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/plugins/marketplace/route.ts +3 -1
- package/src/app/api/plugins/route.ts +3 -1
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -12
- package/src/app/api/providers/configs/route.ts +3 -1
- package/src/app/api/providers/route.ts +7 -3
- package/src/app/api/schedules/[id]/route.ts +16 -15
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +8 -3
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +5 -3
- package/src/app/api/sessions/[id]/chat/route.ts +5 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
- package/src/app/api/sessions/[id]/messages/route.ts +2 -1
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +2 -1
- package/src/app/api/sessions/route.ts +11 -4
- package/src/app/api/settings/route.ts +3 -1
- package/src/app/api/setup/doctor/route.ts +1 -0
- package/src/app/api/setup/openclaw-device/route.ts +3 -1
- package/src/app/api/skills/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +5 -3
- package/src/app/api/tasks/[id]/approve/route.ts +74 -0
- package/src/app/api/tasks/[id]/route.ts +9 -5
- package/src/app/api/tasks/route.ts +5 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- package/src/app/api/usage/route.ts +3 -1
- package/src/app/api/version/route.ts +3 -1
- package/src/app/api/webhooks/[id]/route.ts +31 -32
- package/src/app/api/webhooks/route.ts +5 -3
- package/src/app/icon.svg +58 -0
- package/src/app/page.tsx +11 -26
- package/src/cli/index.js +28 -9
- package/src/cli/index.ts +45 -2
- package/src/cli/spec.js +2 -8
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-list.tsx +3 -1
- package/src/components/agents/agent-sheet.tsx +166 -81
- package/src/components/chat/chat-area.tsx +71 -34
- package/src/components/chat/chat-header.tsx +141 -29
- package/src/components/chat/chat-tool-toggles.tsx +12 -53
- package/src/components/chat/message-bubble.tsx +110 -42
- package/src/components/chat/tool-call-bubble.tsx +50 -6
- package/src/components/chat/tool-request-banner.tsx +1 -9
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +9 -10
- package/src/components/connectors/connector-sheet.tsx +55 -36
- package/src/components/input/chat-input.tsx +72 -56
- package/src/components/knowledge/knowledge-list.tsx +27 -31
- package/src/components/layout/app-layout.tsx +133 -90
- package/src/components/layout/daemon-indicator.tsx +3 -5
- package/src/components/logs/log-list.tsx +5 -9
- package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
- package/src/components/memory/memory-detail.tsx +1 -1
- package/src/components/plugins/plugin-list.tsx +227 -27
- package/src/components/projects/project-list.tsx +122 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/providers/provider-list.tsx +46 -13
- package/src/components/providers/provider-sheet.tsx +0 -45
- package/src/components/runs/run-list.tsx +6 -15
- package/src/components/schedules/schedule-card.tsx +54 -4
- package/src/components/schedules/schedule-list.tsx +9 -4
- package/src/components/schedules/schedule-sheet.tsx +0 -47
- package/src/components/secrets/secrets-list.tsx +20 -2
- package/src/components/sessions/new-session-sheet.tsx +14 -15
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/shared/connector-platform-icon.tsx +26 -20
- package/src/components/shared/model-combobox.tsx +148 -0
- package/src/components/shared/settings/section-heartbeat.tsx +8 -40
- package/src/components/shared/settings/section-orchestrator.tsx +9 -11
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +73 -0
- package/src/components/skills/skill-list.tsx +262 -35
- package/src/components/skills/skill-sheet.tsx +0 -45
- package/src/components/tasks/task-board.tsx +3 -6
- package/src/components/tasks/task-card.tsx +43 -1
- package/src/components/tasks/task-list.tsx +8 -7
- package/src/components/tasks/task-sheet.tsx +0 -44
- package/src/components/usage/usage-list.tsx +12 -4
- package/src/hooks/use-continuous-speech.ts +144 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/hooks/use-ws.ts +66 -0
- package/src/instrumentation.ts +2 -0
- package/src/lib/chat.ts +14 -2
- package/src/lib/id.ts +6 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +15 -2
- package/src/lib/providers/index.ts +8 -0
- package/src/lib/providers/ollama.ts +10 -2
- package/src/lib/providers/openai.ts +42 -13
- package/src/lib/providers/openclaw.ts +11 -0
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +57 -8
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
- package/src/lib/server/connectors/bluebubbles.ts +357 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +46 -7
- package/src/lib/server/connectors/manager.ts +401 -6
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +64 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- package/src/lib/server/context-manager.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -0
- package/src/lib/server/data-dir.ts +1 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +67 -3
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/langgraph-checkpoint.ts +274 -0
- package/src/lib/server/main-agent-loop.ts +67 -8
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-sync.ts +496 -0
- package/src/lib/server/orchestrator-lg.ts +422 -20
- package/src/lib/server/orchestrator.ts +29 -9
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +39 -13
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +8 -3
- package/src/lib/server/session-tools/connector.ts +51 -4
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +5 -5
- package/src/lib/server/session-tools/file.ts +176 -3
- package/src/lib/server/session-tools/index.ts +4 -0
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +197 -0
- package/src/lib/server/session-tools/search-providers.ts +270 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/web.ts +47 -66
- package/src/lib/server/storage-mcp.test.ts +25 -2
- package/src/lib/server/storage.ts +36 -7
- package/src/lib/server/stream-agent-chat.ts +106 -22
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/server/task-validation.test.ts +23 -0
- package/src/lib/server/task-validation.ts +5 -3
- package/src/lib/server/ws-hub.ts +85 -0
- package/src/lib/tool-definitions.ts +44 -0
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/upload.ts +7 -1
- package/src/lib/view-routes.ts +28 -0
- package/src/lib/ws-client.ts +124 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +28 -1
- package/src/stores/use-chat-store.ts +42 -14
- package/src/types/index.ts +34 -2
- package/src/app/api/agents/generate/route.ts +0 -42
- package/src/app/api/generate/info/route.ts +0 -12
- package/src/app/api/generate/route.ts +0 -106
- package/src/app/favicon.ico +0 -0
- 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
|
+
}
|