@cc-soul/openclaw 1.0.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.
package/hub/server.ts ADDED
@@ -0,0 +1,447 @@
1
+ /**
2
+ * Knowledge Hub — cc-soul 知识网络中心 (SQLite 版)
3
+ *
4
+ * 存储引擎:SQLite(单文件,支持索引,百万条毫秒级查询)
5
+ * 运行:npx tsx hub/server.ts
6
+ * 依赖:better-sqlite3(npm install better-sqlite3)
7
+ *
8
+ * API:
9
+ * POST /federation/upload ← cc-soul 实例上传知识
10
+ * GET /federation/download ← cc-soul 实例拉取知识
11
+ * GET /federation/stats ← 查看网络状态
12
+ * POST /federation/register ← 注册新 API key(需 admin key)
13
+ * POST /sync/push ← 跨设备同步上传(私有)
14
+ * GET /sync/pull ← 跨设备同步下载(私有)
15
+ */
16
+
17
+ import { createServer, type IncomingMessage, type ServerResponse } from 'http'
18
+ import { mkdirSync, readFileSync } from 'fs'
19
+ import { resolve } from 'path'
20
+ import { createHash } from 'crypto'
21
+ import Database from 'better-sqlite3'
22
+
23
+ // ── Config ──
24
+ const PORT = parseInt(process.env.HUB_PORT || '9900')
25
+ const DATA_DIR = process.env.HUB_DATA || resolve(import.meta.dirname || '.', 'data')
26
+ const DB_PATH = resolve(DATA_DIR, 'hub.db')
27
+ const MIN_QUALITY = 5
28
+
29
+ // ── Init ──
30
+ mkdirSync(DATA_DIR, { recursive: true })
31
+
32
+ // ── Database ──
33
+ const db = new Database(DB_PATH)
34
+ db.pragma('journal_mode = WAL') // Write-Ahead Logging: 并发读不阻塞
35
+ db.pragma('busy_timeout = 5000')
36
+
37
+ // ── Schema ──
38
+ db.exec(`
39
+ CREATE TABLE IF NOT EXISTS api_keys (
40
+ key TEXT PRIMARY KEY,
41
+ instance_id TEXT NOT NULL,
42
+ instance_name TEXT DEFAULT '',
43
+ created_at INTEGER NOT NULL,
44
+ uploads INTEGER DEFAULT 0,
45
+ downloads INTEGER DEFAULT 0
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS memories (
49
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
50
+ content TEXT NOT NULL,
51
+ scope TEXT NOT NULL,
52
+ tags TEXT,
53
+ source_instance TEXT NOT NULL,
54
+ source_quality REAL DEFAULT 5.0,
55
+ timestamp INTEGER NOT NULL,
56
+ content_hash TEXT NOT NULL UNIQUE,
57
+ received_at INTEGER NOT NULL
58
+ );
59
+
60
+ CREATE TABLE IF NOT EXISTS sync_data (
61
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
62
+ instance_id TEXT NOT NULL,
63
+ data TEXT NOT NULL,
64
+ pushed_at INTEGER NOT NULL
65
+ );
66
+
67
+ CREATE INDEX IF NOT EXISTS idx_memories_received ON memories(received_at);
68
+ CREATE INDEX IF NOT EXISTS idx_memories_source ON memories(source_instance);
69
+ CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(content_hash);
70
+ CREATE INDEX IF NOT EXISTS idx_sync_instance ON sync_data(instance_id);
71
+ `)
72
+
73
+ // Add reports column if not exists (safe for existing databases)
74
+ try { db.exec(`ALTER TABLE memories ADD COLUMN reports INTEGER DEFAULT 0`) } catch { /* already exists */ }
75
+
76
+ // ── Prepared statements (performance) ──
77
+ const stmts = {
78
+ insertMemory: db.prepare(`
79
+ INSERT OR IGNORE INTO memories (content, scope, tags, source_instance, source_quality, timestamp, content_hash, received_at)
80
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
81
+ `),
82
+ getMemories: db.prepare(`
83
+ SELECT * FROM memories WHERE received_at >= ? AND source_instance != ? ORDER BY received_at DESC LIMIT 1000
84
+ `),
85
+ countMemories: db.prepare(`SELECT COUNT(*) as count FROM memories`),
86
+ getApiKey: db.prepare(`SELECT * FROM api_keys WHERE key = ?`),
87
+ insertApiKey: db.prepare(`
88
+ INSERT INTO api_keys (key, instance_id, instance_name, created_at) VALUES (?, ?, ?, ?)
89
+ `),
90
+ updateApiKeyUploads: db.prepare(`UPDATE api_keys SET uploads = uploads + 1 WHERE key = ?`),
91
+ updateApiKeyDownloads: db.prepare(`UPDATE api_keys SET downloads = downloads + 1 WHERE key = ?`),
92
+ getAllApiKeys: db.prepare(`SELECT instance_name, uploads, downloads FROM api_keys`),
93
+ countInstances: db.prepare(`SELECT COUNT(*) as count FROM api_keys`),
94
+ upsertSync: db.prepare(`
95
+ INSERT INTO sync_data (instance_id, data, pushed_at) VALUES (?, ?, ?)
96
+ `),
97
+ getSyncData: db.prepare(`
98
+ SELECT data FROM sync_data WHERE instance_id != ? ORDER BY pushed_at DESC LIMIT 100
99
+ `),
100
+ cleanOldSync: db.prepare(`DELETE FROM sync_data WHERE pushed_at < ?`),
101
+ }
102
+
103
+ // Batch insert transaction
104
+ const insertMemoriesBatch = db.transaction((memories: any[]) => {
105
+ let accepted = 0
106
+ for (const m of memories) {
107
+ const result = stmts.insertMemory.run(
108
+ m.content, m.scope, JSON.stringify(m.tags || []),
109
+ m.sourceInstance, m.sourceQuality,
110
+ m.timestamp, m.contentHash, Date.now()
111
+ )
112
+ if (result.changes > 0) accepted++
113
+ }
114
+ return accepted
115
+ })
116
+
117
+ // ── PII filter (server-side double-check) ──
118
+ const PII_PATTERNS = [
119
+ /\b1[3-9]\d{9}\b/g,
120
+ /\b\d{17}[\dXx]\b/g,
121
+ /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/gi,
122
+ /\b(?:sk-|api[_-]?key|token|secret|password)[=:]\s*\S+/gi,
123
+ /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
124
+ ]
125
+
126
+ function hasPII(text: string): boolean {
127
+ return PII_PATTERNS.some(p => { p.lastIndex = 0; return p.test(text) })
128
+ }
129
+
130
+ function contentHash(s: string): string {
131
+ return createHash('sha256').update(s).digest('hex').slice(0, 16)
132
+ }
133
+
134
+ // ── HTTP helpers ──
135
+ const MAX_BODY_SIZE = 10 * 1024 * 1024 // 10MB max
136
+
137
+ async function readBody(req: IncomingMessage): Promise<string> {
138
+ return new Promise((resolve, reject) => {
139
+ const chunks: Buffer[] = []
140
+ let size = 0
141
+ req.on('data', (chunk: Buffer) => {
142
+ size += chunk.length
143
+ if (size > MAX_BODY_SIZE) {
144
+ req.destroy()
145
+ reject(new Error('body too large'))
146
+ return
147
+ }
148
+ chunks.push(chunk)
149
+ })
150
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
151
+ req.on('error', reject)
152
+ })
153
+ }
154
+
155
+ function json(res: ServerResponse, status: number, data: any) {
156
+ res.writeHead(status, {
157
+ 'Content-Type': 'application/json',
158
+ 'Access-Control-Allow-Origin': '*',
159
+ })
160
+ res.end(JSON.stringify(data))
161
+ }
162
+
163
+ // ── Routes ──
164
+ const server = createServer(async (req, res) => {
165
+ const url = new URL(req.url || '/', `http://localhost:${PORT}`)
166
+ const path = url.pathname
167
+ const method = req.method || 'GET'
168
+
169
+ // Dashboard
170
+ if (method === 'GET' && path === '/dashboard') {
171
+ try {
172
+ const html = readFileSync(resolve(import.meta.dirname || '.', 'dashboard.html'), 'utf-8')
173
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
174
+ return res.end(html)
175
+ } catch {
176
+ res.writeHead(404)
177
+ return res.end('dashboard.html not found')
178
+ }
179
+ }
180
+
181
+ // CORS preflight
182
+ if (method === 'OPTIONS') {
183
+ res.writeHead(200, {
184
+ 'Access-Control-Allow-Origin': '*',
185
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
186
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Instance-Id, X-Quality-Score, X-Admin-Key',
187
+ })
188
+ return res.end()
189
+ }
190
+
191
+ try {
192
+ // ── Federation: Upload ──
193
+ if (method === 'POST' && path === '/federation/upload') {
194
+ const apiKey = (req.headers['authorization'] || '').replace('Bearer ', '')
195
+ const keyEntry = stmts.getApiKey.get(apiKey) as any
196
+ if (!keyEntry) return json(res, 401, { error: 'invalid api key' })
197
+
198
+ const quality = parseFloat(req.headers['x-quality-score'] as string || '0')
199
+ if (quality < MIN_QUALITY) {
200
+ return json(res, 403, { error: `quality too low: ${quality} < ${MIN_QUALITY}` })
201
+ }
202
+
203
+ const body = await readBody(req)
204
+ let data: any
205
+ try { data = JSON.parse(body) } catch { return json(res, 400, { error: 'invalid JSON body' }) }
206
+
207
+ // Filter and prepare
208
+ const valid = (data.memories || [])
209
+ .filter((m: any) => m.content && !hasPII(m.content))
210
+ .map((m: any) => ({
211
+ content: m.content,
212
+ scope: m.scope || 'fact',
213
+ tags: m.tags,
214
+ sourceInstance: m.sourceInstance || keyEntry.instance_id,
215
+ sourceQuality: quality,
216
+ timestamp: m.timestamp || Date.now(),
217
+ contentHash: m.contentHash || contentHash(m.content),
218
+ }))
219
+
220
+ const accepted = insertMemoriesBatch(valid)
221
+ stmts.updateApiKeyUploads.run(apiKey)
222
+
223
+ return json(res, 200, { accepted, total: (data.memories || []).length })
224
+ }
225
+
226
+ // ── Federation: Download ──
227
+ if (method === 'GET' && path === '/federation/download') {
228
+ const apiKey = (req.headers['authorization'] || '').replace('Bearer ', '')
229
+ const keyEntry = stmts.getApiKey.get(apiKey) as any
230
+ if (!keyEntry) return json(res, 401, { error: 'invalid api key' })
231
+
232
+ const since = parseInt(url.searchParams.get('since') || '0')
233
+ const instance = url.searchParams.get('instance') || keyEntry.instance_id
234
+ const memories = stmts.getMemories.all(since, instance) as any[]
235
+
236
+ // Parse tags back from JSON string
237
+ const formatted = memories.map(m => ({
238
+ ...m,
239
+ tags: m.tags ? JSON.parse(m.tags) : [],
240
+ }))
241
+
242
+ stmts.updateApiKeyDownloads.run(apiKey)
243
+ return json(res, 200, { memories: formatted, count: formatted.length })
244
+ }
245
+
246
+ // ── Federation: Stats ──
247
+ if (method === 'GET' && path === '/federation/stats') {
248
+ const totalMemories = (stmts.countMemories.get() as any).count
249
+ const totalInstances = (stmts.countInstances.get() as any).count
250
+ const instances = stmts.getAllApiKeys.all() as any[]
251
+
252
+ return json(res, 200, {
253
+ totalMemories,
254
+ instances: totalInstances,
255
+ instanceList: instances,
256
+ dbSize: `${(db.pragma('page_count', { simple: true }) as number * db.pragma('page_size', { simple: true }) as number / 1024 / 1024).toFixed(2)} MB`,
257
+ })
258
+ }
259
+
260
+ // ── Federation: Auto-Register (no admin key, for cc-soul clients) ──
261
+ if (method === 'POST' && path === '/federation/auto-register') {
262
+ const body = await readBody(req)
263
+ let data: any
264
+ try { data = JSON.parse(body) } catch { return json(res, 400, { error: 'invalid JSON body' }) }
265
+
266
+ const instanceId = data.instanceId
267
+ if (!instanceId) return json(res, 400, { error: 'missing instanceId' })
268
+
269
+ // Check if already registered
270
+ const existing = db.prepare(`SELECT key FROM api_keys WHERE instance_id = ?`).get(instanceId) as any
271
+ if (existing) {
272
+ return json(res, 200, { key: existing.key, instanceId, status: 'existing' })
273
+ }
274
+
275
+ // Rate limit: max 100 registrations per day
276
+ const todayCount = (db.prepare(
277
+ `SELECT COUNT(*) as c FROM api_keys WHERE created_at > ?`
278
+ ).get(Date.now() - 86400000) as any).c
279
+ if (todayCount >= 100) {
280
+ return json(res, 429, { error: 'too many registrations today, try tomorrow' })
281
+ }
282
+
283
+ const newKey = 'csk-' + createHash('sha256')
284
+ .update(Date.now() + Math.random().toString())
285
+ .digest('hex').slice(0, 32)
286
+
287
+ stmts.insertApiKey.run(
288
+ newKey,
289
+ instanceId,
290
+ data.instanceName || 'unnamed',
291
+ Date.now(),
292
+ )
293
+
294
+ console.log(`[hub] auto-registered: ${instanceId} (${data.instanceName || 'unnamed'})`)
295
+ return json(res, 200, { key: newKey, instanceId, status: 'new' })
296
+ }
297
+
298
+ // ── Federation: Register (admin) ──
299
+ if (method === 'POST' && path === '/federation/register') {
300
+ const adminKey = req.headers['x-admin-key'] as string
301
+ if (adminKey !== process.env.HUB_ADMIN_KEY) {
302
+ return json(res, 401, { error: 'invalid admin key' })
303
+ }
304
+
305
+ const body = await readBody(req)
306
+ let data: any
307
+ try { data = JSON.parse(body) } catch { return json(res, 400, { error: 'invalid JSON body' }) }
308
+ const newKey = 'csk-' + createHash('sha256')
309
+ .update(Date.now() + Math.random().toString())
310
+ .digest('hex').slice(0, 32)
311
+
312
+ stmts.insertApiKey.run(
313
+ newKey,
314
+ data.instanceId || 'unknown',
315
+ data.instanceName || 'unnamed',
316
+ Date.now(),
317
+ )
318
+
319
+ return json(res, 200, { key: newKey, instanceId: data.instanceId })
320
+ }
321
+
322
+ // ── Sync: Push ──
323
+ if (method === 'POST' && path === '/sync/push') {
324
+ const instanceId = req.headers['x-instance-id'] as string
325
+ if (!instanceId) return json(res, 400, { error: 'missing X-Instance-Id' })
326
+
327
+ const body = await readBody(req)
328
+
329
+ // Validate JSONL format: each non-empty line must be valid JSON
330
+ const lines = body.split('\n').filter(l => l.trim())
331
+ const validLines: string[] = []
332
+ for (const line of lines) {
333
+ try { JSON.parse(line); validLines.push(line) } catch { /* skip malformed lines */ }
334
+ }
335
+ const validBody = validLines.join('\n')
336
+
337
+ // Clean old sync data for this instance (keep latest only)
338
+ stmts.cleanOldSync.run(Date.now() - 7 * 86400000) // clean >7 days old
339
+ stmts.upsertSync.run(instanceId, validBody, Date.now())
340
+
341
+ return json(res, 200, { ok: true, lines: validLines.length, dropped: lines.length - validLines.length })
342
+ }
343
+
344
+ // ── Sync: Pull ──
345
+ if (method === 'GET' && path === '/sync/pull') {
346
+ const instanceId = req.headers['x-instance-id'] as string || url.searchParams.get('instance') || ''
347
+ if (!instanceId) return json(res, 400, { error: 'missing instance id' })
348
+
349
+ const rows = stmts.getSyncData.all(instanceId) as any[]
350
+ const allData = rows.map(r => r.data).join('\n')
351
+
352
+ res.writeHead(200, { 'Content-Type': 'application/jsonl' })
353
+ return res.end(allData)
354
+ }
355
+
356
+ // ── Health check ──
357
+ if (method === 'GET' && (path === '/' || path === '/health')) {
358
+ return json(res, 200, {
359
+ status: 'ok',
360
+ version: '1.0.0',
361
+ uptime: process.uptime(),
362
+ memories: (stmts.countMemories.get() as any).count,
363
+ instances: (stmts.countInstances.get() as any).count,
364
+ })
365
+ }
366
+
367
+ // ── Federation: Report bad knowledge ──
368
+ if (method === 'POST' && path === '/federation/report') {
369
+ const apiKey = (req.headers['authorization'] || '').replace('Bearer ', '')
370
+ const keyEntry = stmts.getApiKey.get(apiKey) as any
371
+ if (!keyEntry) return json(res, 401, { error: 'invalid api key' })
372
+
373
+ const body = await readBody(req)
374
+ let data: any
375
+ try { data = JSON.parse(body) } catch { return json(res, 400, { error: 'invalid JSON body' }) }
376
+ const hash = data.contentHash
377
+
378
+ if (!hash) return json(res, 400, { error: 'missing contentHash' })
379
+
380
+ const updateReport = db.prepare(`UPDATE memories SET reports = reports + 1 WHERE content_hash = ?`)
381
+ updateReport.run(hash)
382
+
383
+ // Auto-delete if reported 3+ times
384
+ const mem = db.prepare(`SELECT reports FROM memories WHERE content_hash = ?`).get(hash) as any
385
+ if (mem && mem.reports >= 3) {
386
+ db.prepare(`DELETE FROM memories WHERE content_hash = ?`).run(hash)
387
+ console.log(`[hub] auto-deleted memory ${hash} (${mem.reports} reports)`)
388
+ return json(res, 200, { action: 'deleted', reports: mem.reports })
389
+ }
390
+
391
+ return json(res, 200, { action: 'reported', reports: mem?.reports || 1 })
392
+ }
393
+
394
+ // ── 404 ──
395
+ json(res, 404, { error: 'not found', routes: [
396
+ 'GET /health',
397
+ 'POST /federation/upload',
398
+ 'GET /federation/download',
399
+ 'GET /federation/stats',
400
+ 'POST /federation/register',
401
+ 'POST /federation/report',
402
+ 'POST /sync/push',
403
+ 'GET /sync/pull',
404
+ ]})
405
+
406
+ } catch (e: any) {
407
+ console.error(`[hub] ${method} ${path} error: ${e.message}`)
408
+ json(res, 500, { error: e.message })
409
+ }
410
+ })
411
+
412
+ // ── Graceful shutdown ──
413
+ process.on('SIGINT', () => {
414
+ console.log('\n[hub] shutting down...')
415
+ db.close()
416
+ process.exit(0)
417
+ })
418
+
419
+ process.on('SIGTERM', () => {
420
+ db.close()
421
+ process.exit(0)
422
+ })
423
+
424
+ server.listen(PORT, () => {
425
+ const memCount = (stmts.countMemories.get() as any).count
426
+ const instanceCount = (stmts.countInstances.get() as any).count
427
+
428
+ console.log(`
429
+ 🧠 cc-soul Knowledge Hub (SQLite)
430
+ listening on http://0.0.0.0:${PORT}
431
+ database: ${DB_PATH}
432
+ memories: ${memCount}
433
+ instances: ${instanceCount}
434
+
435
+ API:
436
+ GET /health ← 健康检查
437
+ POST /federation/upload ← 实例上传知识
438
+ GET /federation/download ← 实例下载知识
439
+ GET /federation/stats ← 网络状态
440
+ POST /federation/register ← 注册 API key(需 admin key)
441
+ POST /federation/report ← 举报错误知识
442
+ POST /sync/push ← 跨设备同步上传
443
+ GET /sync/pull ← 跨设备同步下载
444
+
445
+ env: HUB_PORT=${PORT} HUB_ADMIN_KEY=${process.env.HUB_ADMIN_KEY ? '(set)' : '⚠️ NOT SET'}
446
+ `)
447
+ })
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@cc-soul/openclaw",
3
+ "version": "1.0.0",
4
+ "description": "Give your AI a soul — cognitive architecture plugin for OpenClaw",
5
+ "type": "module",
6
+ "keywords": ["ai", "soul", "memory", "personality", "openclaw", "cognitive", "agent"],
7
+ "author": "cc-soul",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/cc-soul/cc-soul"
12
+ },
13
+ "bin": {
14
+ "cc-soul": "./scripts/cli.js"
15
+ },
16
+ "files": [
17
+ "cc-soul/",
18
+ "hub/",
19
+ "data/.gitkeep",
20
+ "scripts/",
21
+ "README.md"
22
+ ],
23
+ "openclaw": {
24
+ "hooks": ["./cc-soul"]
25
+ },
26
+ "scripts": {
27
+ "postinstall": "node scripts/install.js"
28
+ }
29
+ }
package/scripts/cli.js ADDED
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cc-soul CLI — enable/disable/status/config commands
4
+ * Usage: cc-soul <command> [args]
5
+ */
6
+ import { existsSync, readFileSync, writeFileSync } from 'fs'
7
+ import { resolve } from 'path'
8
+ import { homedir } from 'os'
9
+
10
+ const DATA_DIR = resolve(homedir(), '.openclaw/hooks/cc-soul/data')
11
+ const FEATURES_PATH = resolve(DATA_DIR, 'features.json')
12
+ const SYNC_PATH = resolve(DATA_DIR, 'sync_config.json')
13
+ const AI_PATH = resolve(DATA_DIR, 'ai_config.json')
14
+
15
+ function loadJson(path, fallback) {
16
+ try { return existsSync(path) ? JSON.parse(readFileSync(path, 'utf-8')) : fallback }
17
+ catch { return fallback }
18
+ }
19
+
20
+ const [,, cmd, ...args] = process.argv
21
+
22
+ switch (cmd) {
23
+ case 'status': {
24
+ const features = loadJson(FEATURES_PATH, {})
25
+ const sync = loadJson(SYNC_PATH, {})
26
+ console.log('\n🧠 cc-soul status\n')
27
+ console.log('Features:')
28
+ for (const [k, v] of Object.entries(features)) {
29
+ if (k.startsWith('_')) continue
30
+ console.log(` ${v ? '✅' : '❌'} ${k}`)
31
+ }
32
+ console.log(`\nSync: ${sync.enabled ? 'ON' : 'OFF'}`)
33
+ console.log(`Federation: ${sync.federationEnabled ? 'ON' : 'OFF'}`)
34
+ console.log(`Instance: ${sync.instanceId || 'not set'}`)
35
+ console.log(`Hub: ${sync.hubUrl || 'not configured'}`)
36
+ break
37
+ }
38
+
39
+ case 'enable': {
40
+ if (!args[0]) { console.log('Usage: cc-soul enable <feature>'); break }
41
+ const features = loadJson(FEATURES_PATH, {})
42
+ if (!(args[0] in features)) { console.log(`Unknown feature: ${args[0]}`); break }
43
+ features[args[0]] = true
44
+ writeFileSync(FEATURES_PATH, JSON.stringify(features, null, 2))
45
+ console.log(`✅ ${args[0]} enabled`)
46
+ console.log('Restart gateway: pkill -HUP -f "openclaw.*gateway"')
47
+ break
48
+ }
49
+
50
+ case 'disable': {
51
+ if (!args[0]) { console.log('Usage: cc-soul disable <feature>'); break }
52
+ const features = loadJson(FEATURES_PATH, {})
53
+ if (!(args[0] in features)) { console.log(`Unknown feature: ${args[0]}`); break }
54
+ features[args[0]] = false
55
+ writeFileSync(FEATURES_PATH, JSON.stringify(features, null, 2))
56
+ console.log(`❌ ${args[0]} disabled`)
57
+ console.log('Restart gateway: pkill -HUP -f "openclaw.*gateway"')
58
+ break
59
+ }
60
+
61
+ case 'config': {
62
+ const [subCmd, key, value] = args
63
+ if (subCmd === 'set' && key && value) {
64
+ if (['hub_url', 'hubUrl'].includes(key)) {
65
+ const sync = loadJson(SYNC_PATH, {})
66
+ sync.hubUrl = value
67
+ sync.enabled = true
68
+ writeFileSync(SYNC_PATH, JSON.stringify(sync, null, 2))
69
+ console.log(`✅ hubUrl = ${value}`)
70
+ } else if (['federation', 'federationEnabled'].includes(key)) {
71
+ const sync = loadJson(SYNC_PATH, {})
72
+ sync.federationEnabled = value === 'true'
73
+ writeFileSync(SYNC_PATH, JSON.stringify(sync, null, 2))
74
+ console.log(`✅ federation = ${value}`)
75
+ } else if (['ai_backend', 'backend'].includes(key)) {
76
+ const ai = loadJson(AI_PATH, {})
77
+ ai.backend = value
78
+ writeFileSync(AI_PATH, JSON.stringify(ai, null, 2))
79
+ console.log(`✅ AI backend = ${value}`)
80
+ } else if (['ai_api_base', 'api_base'].includes(key)) {
81
+ const ai = loadJson(AI_PATH, {})
82
+ ai.backend = 'openai-compatible'
83
+ ai.api_base = value
84
+ writeFileSync(AI_PATH, JSON.stringify(ai, null, 2))
85
+ console.log(`✅ API base = ${value}`)
86
+ } else if (['ai_api_key', 'api_key'].includes(key)) {
87
+ const ai = loadJson(AI_PATH, {})
88
+ ai.api_key = value
89
+ writeFileSync(AI_PATH, JSON.stringify(ai, null, 2))
90
+ console.log(`✅ API key set`)
91
+ } else if (['ai_model', 'model'].includes(key)) {
92
+ const ai = loadJson(AI_PATH, {})
93
+ ai.api_model = value
94
+ writeFileSync(AI_PATH, JSON.stringify(ai, null, 2))
95
+ console.log(`✅ model = ${value}`)
96
+ } else {
97
+ console.log(`Unknown config key: ${key}`)
98
+ }
99
+ } else {
100
+ console.log('Usage: cc-soul config set <key> <value>')
101
+ console.log('Keys: hub_url, federation, ai_backend, ai_api_base, ai_api_key, ai_model')
102
+ }
103
+ break
104
+ }
105
+
106
+ case 'install': {
107
+ console.log('Running install script...')
108
+ import('./install.js')
109
+ break
110
+ }
111
+
112
+ default:
113
+ console.log(`
114
+ 🧠 cc-soul — Give your AI a soul
115
+
116
+ Commands:
117
+ cc-soul status Show features & sync status
118
+ cc-soul enable <feature> Enable a feature
119
+ cc-soul disable <feature> Disable a feature
120
+ cc-soul config set <key> <value> Set configuration
121
+
122
+ Config keys:
123
+ hub_url <url> Knowledge Hub URL
124
+ federation true/false Enable/disable federation
125
+ ai_backend cli/openai-compatible
126
+ ai_api_base <url> API base URL
127
+ ai_api_key <key> API key
128
+ ai_model <model> Model name
129
+
130
+ Examples:
131
+ cc-soul enable dream_mode
132
+ cc-soul disable self_upgrade
133
+ cc-soul config set hub_url https://hub.example.com:9900
134
+ cc-soul config set ai_api_base https://api.openai.com/v1
135
+ `)
136
+ }