woclaw-hub 0.1.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/Dockerfile ADDED
@@ -0,0 +1,41 @@
1
+ FROM node:18-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # Install build dependencies for better-sqlite3
6
+ RUN apk add --no-cache \
7
+ python3 \
8
+ make \
9
+ g++ \
10
+ && rm -rf /var/cache/apk/*
11
+
12
+ # Copy package files
13
+ COPY package*.json ./
14
+
15
+ # Install dependencies
16
+ RUN npm ci --only=production
17
+
18
+ # Copy source (pre-built)
19
+ COPY dist ./dist
20
+
21
+ # Create data directory
22
+ RUN mkdir -p /data && chown -R node:node /data
23
+
24
+ # Expose ports
25
+ EXPOSE 8080 8081
26
+
27
+ # Health check
28
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
29
+ CMD wget --no-verbose --tries=1 --spider http://localhost:8081/health || exit 1
30
+
31
+ # Run as non-root user
32
+ USER node
33
+
34
+ ENV NODE_ENV=production
35
+ ENV HOST=0.0.0.0
36
+ ENV PORT=8080
37
+ ENV REST_PORT=8081
38
+ ENV DATA_DIR=/data
39
+ ENV AUTH_TOKEN=change-me-in-production
40
+
41
+ CMD ["node", "dist/index.js"]
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # WoClaw Hub
2
+
3
+ WebSocket relay server for OpenClaw multi-agent communication.
4
+
5
+ ## Quick Start
6
+
7
+ ### Using Docker
8
+
9
+ ```bash
10
+ # Build
11
+ docker build -t woclaw/hub .
12
+
13
+ # Run
14
+ docker run -d \
15
+ --name woclaw-hub \
16
+ -p 8080:8080 \
17
+ -p 8081:8081 \
18
+ -v /path/to/data:/data \
19
+ -e AUTH_TOKEN=your-secure-token \
20
+ --restart unless-stopped \
21
+ woclaw/hub
22
+ ```
23
+
24
+ ### From Source
25
+
26
+ ```bash
27
+ cd hub
28
+ npm install
29
+ npm run build
30
+ npm start
31
+ ```
32
+
33
+ ## Environment Variables
34
+
35
+ | Variable | Default | Description |
36
+ |----------|---------|-------------|
37
+ | `PORT` | 8080 | WebSocket server port |
38
+ | `REST_PORT` | 8081 | REST API port (future) |
39
+ | `HOST` | 0.0.0.0 | Bind address |
40
+ | `DATA_DIR` | /data | SQLite database directory |
41
+ | `AUTH_TOKEN` | change-me | Authentication token |
42
+ | `CONFIG_FILE` | - | JSON config file path |
43
+
44
+ ## WebSocket API
45
+
46
+ ### Connect
47
+
48
+ ```javascript
49
+ const ws = new WebSocket('ws://localhost:8080?agentId=vm151&token=your-token');
50
+ ```
51
+
52
+ ### Send Message
53
+
54
+ ```javascript
55
+ ws.send(JSON.stringify({
56
+ type: 'message',
57
+ topic: 'openclaw-general',
58
+ content: 'Hello from vm151!'
59
+ }));
60
+ ```
61
+
62
+ ### Join Topic
63
+
64
+ ```javascript
65
+ ws.send(JSON.stringify({
66
+ type: 'join',
67
+ topic: 'openclaw-general'
68
+ }));
69
+ ```
70
+
71
+ ### Leave Topic
72
+
73
+ ```javascript
74
+ ws.send(JSON.stringify({
75
+ type: 'leave',
76
+ topic: 'openclaw-general'
77
+ }));
78
+ ```
79
+
80
+ ### Write to Shared Memory
81
+
82
+ ```javascript
83
+ ws.send(JSON.stringify({
84
+ type: 'memory_write',
85
+ key: 'project-status',
86
+ value: { status: 'in-progress', updated: new Date().toISOString() }
87
+ }));
88
+ ```
89
+
90
+ ### Read Memory
91
+
92
+ ```javascript
93
+ ws.send(JSON.stringify({
94
+ type: 'memory_read',
95
+ key: 'project-status'
96
+ }));
97
+ ```
98
+
99
+ ### List Topics
100
+
101
+ ```javascript
102
+ ws.send(JSON.stringify({
103
+ type: 'topics_list'
104
+ }));
105
+ ```
106
+
107
+ ### Get Topic Members
108
+
109
+ ```javascript
110
+ ws.send(JSON.stringify({
111
+ type: 'topic_members',
112
+ topic: 'openclaw-general'
113
+ }));
114
+ ```
115
+
116
+ ## Server Responses
117
+
118
+ ```javascript
119
+ // New message
120
+ { "type": "message", "topic": "...", "from": "vm151", "content": "...", "timestamp": 1234567890 }
121
+
122
+ // Join confirmation
123
+ { "type": "join", "topic": "...", "agent": "vm151", "timestamp": 1234567890 }
124
+
125
+ // Leave notification
126
+ { "type": "leave", "topic": "...", "agent": "vm151", "timestamp": 1234567890 }
127
+
128
+ // Message history (on join)
129
+ { "type": "history", "topic": "...", "messages": [...], "agents": [...] }
130
+
131
+ // Memory update broadcast
132
+ { "type": "memory_update", "key": "...", "value": "...", "from": "vm151", "timestamp": 1234567890 }
133
+
134
+ // Memory value response
135
+ { "type": "memory_value", "key": "...", "value": "...", "exists": true, "updatedAt": ..., "updatedBy": "vm151" }
136
+
137
+ // Topics list
138
+ { "type": "topics_list", "topics": [{ "name": "...", "agents": 3 }] }
139
+
140
+ // Topic members
141
+ { "type": "topic_members", "topic": "...", "agents": ["vm151", "vm152"] }
142
+
143
+ // Error
144
+ { "type": "error", "code": "...", "message": "...", "timestamp": 1234567890 }
145
+ ```
146
+
147
+ ## Architecture
148
+
149
+ ```
150
+ ┌─────────────────────────────────────────────────────────────┐
151
+ │ WoClaw Hub │
152
+ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
153
+ │ │ Topics Mgr │ │ Memory Pool │ │ SQLite │ │
154
+ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
155
+ │ │ │ │ │
156
+ │ └────────────────┼────────────────┘ │
157
+ │ │ │
158
+ │ ┌───────┴───────┐ │
159
+ │ │ WSServer │ │
160
+ │ └───────┬───────┘ │
161
+ └──────────────────────────┼───────────────────────────────────┘
162
+
163
+ ┌─────────────────┼─────────────────┐
164
+ │ │ │
165
+ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐
166
+ │ vm151 │ │ vm152 │ │ vm153 │
167
+ │ (p14) │ │ │ │ │
168
+ └─────────┘ └─────────┘ └─────────┘
169
+ ```
170
+
171
+ ## License
172
+
173
+ MIT
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "woclaw-hub",
3
+ "version": "0.1.0",
4
+ "description": "WoClaw Hub - WebSocket relay server for OpenClaw multi-agent communication",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "start": "node dist/index.js",
10
+ "dev": "tsx watch src/index.ts",
11
+ "docker:build": "docker build -t woclaw/hub ."
12
+ },
13
+ "dependencies": {
14
+ "ws": "^8.16.0",
15
+ "uuid": "^9.0.1"
16
+ },
17
+ "devDependencies": {
18
+ "@types/uuid": "^9.0.7",
19
+ "@types/ws": "^8.5.10",
20
+ "tsx": "^4.7.1",
21
+ "typescript": "^5.3.3"
22
+ },
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ }
26
+ }
package/src/db.ts ADDED
@@ -0,0 +1,128 @@
1
+ // WoClaw Database - Simple JSON File Store
2
+ // No native compilation required!
3
+
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
5
+ import { dirname } from 'path';
6
+ import { DBMessage, DBMemory } from './types.js';
7
+
8
+ export class ClawDB {
9
+ private dbPath: string;
10
+ private data: {
11
+ messages: DBMessage[];
12
+ memory: { key: string; value: string; updatedAt: number; updatedBy: string }[];
13
+ topics: { name: string; createdAt: number; messageCount: number }[];
14
+ };
15
+
16
+ constructor(dataDir: string) {
17
+ if (!existsSync(dataDir)) {
18
+ mkdirSync(dataDir, { recursive: true });
19
+ }
20
+ this.dbPath = `${dataDir}/woclaw.json`;
21
+ this.data = this.load();
22
+ }
23
+
24
+ private load() {
25
+ if (existsSync(this.dbPath)) {
26
+ try {
27
+ return JSON.parse(readFileSync(this.dbPath, 'utf-8'));
28
+ } catch (e) {
29
+ console.error('[ClawDB] Failed to load DB, starting fresh');
30
+ }
31
+ }
32
+ return {
33
+ messages: [],
34
+ memory: [],
35
+ topics: [],
36
+ };
37
+ }
38
+
39
+ private save() {
40
+ try {
41
+ writeFileSync(this.dbPath, JSON.stringify(this.data, null, 2));
42
+ } catch (e) {
43
+ console.error('[ClawDB] Failed to save:', e);
44
+ }
45
+ }
46
+
47
+ // Messages
48
+ saveMessage(msg: DBMessage): void {
49
+ this.data.messages.push(msg);
50
+ // Keep last 1000 messages per topic (simple cleanup)
51
+ if (this.data.messages.length > 10000) {
52
+ this.data.messages = this.data.messages.slice(-5000);
53
+ }
54
+
55
+ // Update topic stats
56
+ const topic = this.data.topics.find(t => t.name === msg.topic);
57
+ if (topic) {
58
+ topic.messageCount++;
59
+ } else {
60
+ this.data.topics.push({ name: msg.topic, createdAt: Date.now(), messageCount: 1 });
61
+ }
62
+ this.save();
63
+ }
64
+
65
+ getMessages(topic: string, limit: number = 100, before?: number): DBMessage[] {
66
+ let msgs = this.data.messages.filter(m => m.topic === topic);
67
+ if (before) {
68
+ msgs = msgs.filter(m => m.timestamp < before);
69
+ }
70
+ return msgs.slice(-limit).reverse();
71
+ }
72
+
73
+ // Memory
74
+ setMemory(key: string, value: string, updatedBy: string): void {
75
+ const existing = this.data.memory.find(m => m.key === key);
76
+ if (existing) {
77
+ existing.value = value;
78
+ existing.updatedAt = Date.now();
79
+ existing.updatedBy = updatedBy;
80
+ } else {
81
+ this.data.memory.push({ key, value, updatedAt: Date.now(), updatedBy });
82
+ }
83
+ this.save();
84
+ }
85
+
86
+ getMemory(key: string): DBMemory | undefined {
87
+ const m = this.data.memory.find(mem => mem.key === key);
88
+ if (!m) return undefined;
89
+ return {
90
+ key: m.key,
91
+ value: m.value,
92
+ updatedAt: m.updatedAt,
93
+ updatedBy: m.updatedBy,
94
+ };
95
+ }
96
+
97
+ deleteMemory(key: string): boolean {
98
+ const idx = this.data.memory.findIndex(m => m.key === key);
99
+ if (idx >= 0) {
100
+ this.data.memory.splice(idx, 1);
101
+ this.save();
102
+ return true;
103
+ }
104
+ return false;
105
+ }
106
+
107
+ getAllMemory(): DBMemory[] {
108
+ return this.data.memory.map(m => ({
109
+ key: m.key,
110
+ value: m.value,
111
+ updatedAt: m.updatedAt,
112
+ updatedBy: m.updatedBy,
113
+ }));
114
+ }
115
+
116
+ // Topics
117
+ getTopicStats(): { name: string; messageCount: number; createdAt: number }[] {
118
+ return this.data.topics.map(t => ({
119
+ name: t.name,
120
+ messageCount: t.messageCount,
121
+ createdAt: t.createdAt,
122
+ }));
123
+ }
124
+
125
+ close(): void {
126
+ this.save();
127
+ }
128
+ }
package/src/index.ts ADDED
@@ -0,0 +1,82 @@
1
+ import { WSServer } from './ws_server.js';
2
+ import { RestServer } from './rest_server.js';
3
+ import { ClawDB } from './db.js';
4
+ import { Config } from './types.js';
5
+ import { readFileSync } from 'fs';
6
+
7
+ const DEFAULT_CONFIG: Config = {
8
+ port: parseInt(process.env.PORT || '8080'),
9
+ restPort: parseInt(process.env.REST_PORT || '8081'),
10
+ host: process.env.HOST || '0.0.0.0',
11
+ dataDir: process.env.DATA_DIR || '/data',
12
+ authToken: process.env.AUTH_TOKEN || 'change-me-in-production',
13
+ };
14
+
15
+ async function main() {
16
+ console.log(`
17
+ ██████╗ ███████╗██╗ ██╗ ██╗ ██╗███╗ ██╗██╗ ██╗██╗ ██╗
18
+ ██╔══██╗██╔════╝██║ ██║ ██║ ██║████╗ ██║██║ ██║╚██╗██╔╝
19
+ ██║ ██║█████╗ ██║ ██║ ██║ ██║██╔██╗ ██║██║ ██║ ╚███╔╝
20
+ ██║ ██║██╔══╝ ╚██╗ ██╔╝ ██║ ██║██║╚██╗██║██║ ██║ ██╔██╗
21
+ ██████╔╝███████╗ ╚████╔╝ ███████╗██║██║ ╚████║╚██████╔╝██╔╝ ██╗
22
+ ╚═════╝ ╚══════╝ ╚═══╝ ╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝
23
+
24
+ OpenClaw Multi-Agent Communication Hub
25
+ `);
26
+
27
+ // Load config from environment or file
28
+ let config = DEFAULT_CONFIG;
29
+ const configPath = process.env.CONFIG_FILE;
30
+ if (configPath) {
31
+ try {
32
+ const fileConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
33
+ config = { ...config, ...fileConfig };
34
+ console.log(`[WoClaw] Loaded config from ${configPath}`);
35
+ } catch (e) {
36
+ console.error(`[WoClaw] Failed to load config: ${e}`);
37
+ process.exit(1);
38
+ }
39
+ }
40
+
41
+ console.log(`[WoClaw] Configuration:`);
42
+ console.log(` WebSocket Port: ${config.port}`);
43
+ console.log(` REST Port: ${config.restPort}`);
44
+ console.log(` Host: ${config.host}`);
45
+ console.log(` Data Dir: ${config.dataDir}`);
46
+ console.log(` Auth Token: ${config.authToken.substring(0, 8)}...`);
47
+ console.log('');
48
+
49
+ // Initialize database
50
+ const db = new ClawDB(config.dataDir);
51
+ console.log('[WoClaw] Database initialized');
52
+
53
+ // Initialize WebSocket server (this also creates TopicsManager and MemoryPool internally)
54
+ const wsServer = new WSServer(config, db);
55
+
56
+ // Start REST API server with access to db, topics, memory
57
+ const restServer = new RestServer(config, db, wsServer.getTopicsManager(), wsServer.getMemoryPool());
58
+ restServer.start();
59
+
60
+ console.log('[WoClaw] Server started successfully');
61
+ console.log('');
62
+ console.log('[WoClaw] Endpoints:');
63
+ console.log(` WebSocket: ws://${config.host}:${config.port}`);
64
+ console.log(` REST API: http://${config.host}:${config.restPort}`);
65
+ console.log('');
66
+
67
+ // Graceful shutdown
68
+ const shutdown = () => {
69
+ console.log('[WoClaw] Shutting down...');
70
+ restServer.close();
71
+ wsServer.close();
72
+ process.exit(0);
73
+ };
74
+
75
+ process.on('SIGINT', shutdown);
76
+ process.on('SIGTERM', shutdown);
77
+ }
78
+
79
+ main().catch((e) => {
80
+ console.error('[WoClaw] Fatal error:', e);
81
+ process.exit(1);
82
+ });
package/src/memory.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { ClawDB } from './db.js';
2
+ import { DBMemory, OutboundMessage } from './types.js';
3
+
4
+ export class MemoryPool {
5
+ private db: ClawDB;
6
+ private subscribers: Map<string, (msg: OutboundMessage) => void> = new Map();
7
+
8
+ constructor(db: ClawDB) {
9
+ this.db = db;
10
+ }
11
+
12
+ write(key: string, value: any, updatedBy: string): DBMemory {
13
+ const serialized = typeof value === 'string' ? value : JSON.stringify(value);
14
+ this.db.setMemory(key, serialized, updatedBy);
15
+ return this.db.getMemory(key)!;
16
+ }
17
+
18
+ read(key: string): DBMemory | undefined {
19
+ return this.db.getMemory(key);
20
+ }
21
+
22
+ delete(key: string): boolean {
23
+ return this.db.deleteMemory(key);
24
+ }
25
+
26
+ getAll(): DBMemory[] {
27
+ return this.db.getAllMemory();
28
+ }
29
+
30
+ // For agents that want to subscribe to memory changes
31
+ subscribe(agentId: string, callback: (msg: OutboundMessage) => void): void {
32
+ this.subscribers.set(agentId, callback);
33
+ }
34
+
35
+ unsubscribe(agentId: string): void {
36
+ this.subscribers.delete(agentId);
37
+ }
38
+
39
+ notifySubscribers(message: OutboundMessage): void {
40
+ for (const callback of this.subscribers.values()) {
41
+ try {
42
+ callback(message);
43
+ } catch (e) {
44
+ console.error('Error notifying subscriber:', e);
45
+ }
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,180 @@
1
+ import http from 'http';
2
+ import { ClawDB } from './db.js';
3
+ import { TopicsManager } from './topics.js';
4
+ import { MemoryPool } from './memory.js';
5
+ import { Config } from './types.js';
6
+
7
+ export class RestServer {
8
+ private server: http.Server | null = null;
9
+ private db: ClawDB;
10
+ private topics: TopicsManager;
11
+ private memory: MemoryPool;
12
+ private config: Config;
13
+
14
+ constructor(config: Config, db: ClawDB, topics: TopicsManager, memory: MemoryPool) {
15
+ this.config = config;
16
+ this.db = db;
17
+ this.topics = topics;
18
+ this.memory = memory;
19
+ }
20
+
21
+ start(): void {
22
+ this.server = http.createServer((req, res) => {
23
+ this.handleRequest(req, res);
24
+ });
25
+
26
+ this.server.listen(this.config.restPort, () => {
27
+ console.log(`[WoClaw] REST API running on http://${this.config.host}:${this.config.restPort}`);
28
+ });
29
+ }
30
+
31
+ private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
32
+ // CORS headers
33
+ res.setHeader('Access-Control-Allow-Origin', '*');
34
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
35
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
36
+
37
+ if (req.method === 'OPTIONS') {
38
+ res.writeHead(200);
39
+ res.end();
40
+ return;
41
+ }
42
+
43
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
44
+ const path = url.pathname;
45
+ const method = req.method || 'GET';
46
+
47
+ // Auth check for write operations
48
+ const authHeader = req.headers.authorization;
49
+ if (method !== 'GET' && authHeader !== `Bearer ${this.config.authToken}`) {
50
+ res.writeHead(401, { 'Content-Type': 'application/json' });
51
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
52
+ return;
53
+ }
54
+
55
+ try {
56
+ if (path === '/health') {
57
+ this.handleHealth(res);
58
+ } else if (path === '/topics') {
59
+ if (method === 'GET') {
60
+ this.handleTopicsList(res);
61
+ } else {
62
+ res.writeHead(405);
63
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
64
+ }
65
+ } else if (path === '/memory') {
66
+ if (method === 'GET') {
67
+ this.handleMemoryList(res);
68
+ } else {
69
+ res.writeHead(405);
70
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
71
+ }
72
+ } else if (path.startsWith('/memory/')) {
73
+ const key = decodeURIComponent(path.slice(8));
74
+ if (method === 'GET') {
75
+ this.handleMemoryGet(res, key);
76
+ } else if (method === 'DELETE') {
77
+ this.handleMemoryDelete(res, key);
78
+ } else {
79
+ res.writeHead(405);
80
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
81
+ }
82
+ } else if (path.startsWith('/topics/')) {
83
+ const topicName = decodeURIComponent(path.slice(8));
84
+ if (method === 'GET') {
85
+ this.handleTopicMessages(res, topicName, url.searchParams.get('limit'));
86
+ } else {
87
+ res.writeHead(405);
88
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
89
+ }
90
+ } else {
91
+ res.writeHead(404);
92
+ res.end(JSON.stringify({ error: 'Not found' }));
93
+ }
94
+ } catch (e: any) {
95
+ console.error('[WoClaw] REST error:', e.message);
96
+ res.writeHead(500, { 'Content-Type': 'application/json' });
97
+ res.end(JSON.stringify({ error: e.message }));
98
+ }
99
+ }
100
+
101
+ private handleHealth(res: http.ServerResponse): void {
102
+ const stats = this.topics.getStats();
103
+ res.writeHead(200, { 'Content-Type': 'application/json' });
104
+ res.end(JSON.stringify({
105
+ status: 'ok',
106
+ uptime: process.uptime(),
107
+ timestamp: Date.now(),
108
+ agents: stats.totalAgents,
109
+ topics: stats.totalTopics,
110
+ }));
111
+ }
112
+
113
+ private handleTopicsList(res: http.ServerResponse): void {
114
+ const stats = this.topics.getStats();
115
+ res.writeHead(200, { 'Content-Type': 'application/json' });
116
+ res.end(JSON.stringify({
117
+ topics: stats.topicDetails.map(t => ({
118
+ name: t.name,
119
+ agents: t.agents,
120
+ }))
121
+ }));
122
+ }
123
+
124
+ private handleMemoryList(res: http.ServerResponse): void {
125
+ const allMemory = this.memory.getAll();
126
+ res.writeHead(200, { 'Content-Type': 'application/json' });
127
+ res.end(JSON.stringify({
128
+ memory: allMemory.map(m => ({
129
+ key: m.key,
130
+ updatedAt: m.updatedAt,
131
+ updatedBy: m.updatedBy,
132
+ }))
133
+ }));
134
+ }
135
+
136
+ private handleMemoryGet(res: http.ServerResponse, key: string): void {
137
+ const mem = this.memory.read(key);
138
+ if (!mem) {
139
+ res.writeHead(404, { 'Content-Type': 'application/json' });
140
+ res.end(JSON.stringify({ error: 'Key not found' }));
141
+ return;
142
+ }
143
+ res.writeHead(200, { 'Content-Type': 'application/json' });
144
+ res.end(JSON.stringify({
145
+ key: mem.key,
146
+ value: mem.value,
147
+ updatedAt: mem.updatedAt,
148
+ updatedBy: mem.updatedBy,
149
+ }));
150
+ }
151
+
152
+ private handleMemoryDelete(res: http.ServerResponse, key: string): void {
153
+ const deleted = this.memory.delete(key);
154
+ if (!deleted) {
155
+ res.writeHead(404, { 'Content-Type': 'application/json' });
156
+ res.end(JSON.stringify({ error: 'Key not found' }));
157
+ return;
158
+ }
159
+ res.writeHead(200, { 'Content-Type': 'application/json' });
160
+ res.end(JSON.stringify({ success: true, key }));
161
+ }
162
+
163
+ private handleTopicMessages(res: http.ServerResponse, topic: string, limit?: string | null): void {
164
+ const limitNum = Math.min(parseInt(limit || '50'), 200);
165
+ const messages = this.db.getMessages(topic, limitNum);
166
+ res.writeHead(200, { 'Content-Type': 'application/json' });
167
+ res.end(JSON.stringify({
168
+ topic,
169
+ messages: messages.reverse(),
170
+ count: messages.length,
171
+ }));
172
+ }
173
+
174
+ close(): void {
175
+ if (this.server) {
176
+ this.server.close();
177
+ console.log('[WoClaw] REST server closed');
178
+ }
179
+ }
180
+ }