openclaw-bridge-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/.env.example ADDED
@@ -0,0 +1,6 @@
1
+ PORT=3080
2
+ API_KEY=your-secret-api-key-here
3
+ DATA_DIR=./data
4
+ CLEANUP_INTERVAL_MS=3600000
5
+ FILE_TTL_MS=86400000
6
+ COMMAND_TTL_MS=3600000
@@ -0,0 +1,15 @@
1
+ export default {
2
+ apps: [
3
+ {
4
+ name: "openclaw-bridge-hub",
5
+ script: "src/index.ts",
6
+ interpreter: "node",
7
+ interpreter_args: "--import tsx",
8
+ env: {
9
+ NODE_ENV: "production",
10
+ },
11
+ max_memory_restart: "256M",
12
+ log_date_format: "YYYY-MM-DD HH:mm:ss",
13
+ },
14
+ ],
15
+ };
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "openclaw-bridge-hub",
3
+ "version": "0.1.0",
4
+ "description": "OpenClaw Bridge Hub — gateway registry, file relay, and command queue for distributed OpenClaw instances",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "bin": {
8
+ "openclaw-bridge-hub": "src/cli.ts"
9
+ },
10
+ "scripts": {
11
+ "start": "tsx src/index.ts",
12
+ "build": "tsc",
13
+ "dev": "tsx watch src/index.ts"
14
+ },
15
+ "dependencies": {
16
+ "fastify": "^5.0.0",
17
+ "better-sqlite3": "^11.0.0",
18
+ "uuid": "^11.0.0",
19
+ "dotenv": "^16.4.0",
20
+ "tsx": "^4.19.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/better-sqlite3": "^7.6.0",
24
+ "@types/uuid": "^10.0.0",
25
+ "typescript": "^5.7.0"
26
+ },
27
+ "keywords": ["openclaw", "bridge", "filerelay", "gateway", "registry"],
28
+ "license": "MIT"
29
+ }
package/src/cleanup.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { unlinkSync } from "node:fs";
2
+ import { db } from "./db.js";
3
+ import { config } from "./config.js";
4
+
5
+ export function startCleanup(): ReturnType<typeof setInterval> {
6
+ return setInterval(() => {
7
+ const now = Date.now();
8
+
9
+ const oldFiles = db
10
+ .prepare(
11
+ `SELECT id, diskPath FROM files
12
+ WHERE status = 'acknowledged'
13
+ OR (createdAt < ? AND status = 'pending')`,
14
+ )
15
+ .all(new Date(now - config.fileTtlMs).toISOString()) as Array<{
16
+ id: string;
17
+ diskPath: string;
18
+ }>;
19
+
20
+ for (const f of oldFiles) {
21
+ try { unlinkSync(f.diskPath); } catch {}
22
+ db.prepare(`DELETE FROM files WHERE id = ?`).run(f.id);
23
+ }
24
+
25
+ db.prepare(
26
+ `DELETE FROM commands
27
+ WHERE (status IN ('responded', 'error'))
28
+ OR (createdAt < ? AND status = 'queued')`,
29
+ ).run(new Date(now - config.commandTtlMs).toISOString());
30
+ }, config.cleanupIntervalMs);
31
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env -S npx tsx
2
+ import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { randomBytes } from "node:crypto";
5
+ import { execSync } from "node:child_process";
6
+
7
+ const DATA_DIR = process.env.BRIDGE_FILERELAY_DATA || join(process.env.HOME || "/root", ".openclaw-bridge-hub");
8
+ const ENV_FILE = join(DATA_DIR, ".env");
9
+ const IS_LINUX = process.platform === "linux";
10
+
11
+ const command = process.argv[2];
12
+
13
+ function printHelp() {
14
+ console.log(`
15
+ openclaw-bridge-hub — OpenClaw Bridge FileRelay Server
16
+
17
+ Commands:
18
+ init Initialize config (generates API key, creates data dir)
19
+ start Start the server
20
+ status Check if server is running
21
+ install-service Install as systemd service (Linux, auto-start on boot)
22
+ uninstall-service Remove systemd service
23
+ help Show this help
24
+
25
+ Environment:
26
+ BRIDGE_FILERELAY_DATA Data directory (default: ~/.openclaw-bridge-hub)
27
+ PORT Server port (default: 3080)
28
+ API_KEY API key for authentication
29
+
30
+ Example:
31
+ npm install -g openclaw-bridge-hub
32
+ openclaw-bridge-hub init
33
+ openclaw-bridge-hub start
34
+ openclaw-bridge-hub install-service # auto-start on reboot
35
+ `);
36
+ }
37
+
38
+ function init() {
39
+ mkdirSync(DATA_DIR, { recursive: true });
40
+
41
+ if (existsSync(ENV_FILE)) {
42
+ console.log(`Config already exists at ${ENV_FILE}`);
43
+ console.log("Edit it to change settings, then run: openclaw-bridge-hub start");
44
+ return;
45
+ }
46
+
47
+ const apiKey = randomBytes(32).toString("hex");
48
+ const envContent = `# Bridge FileRelay Configuration
49
+ PORT=3080
50
+ API_KEY=${apiKey}
51
+ DATA_DIR=${DATA_DIR}/data
52
+ CLEANUP_INTERVAL_MS=3600000
53
+ FILE_TTL_MS=86400000
54
+ COMMAND_TTL_MS=3600000
55
+ `;
56
+
57
+ writeFileSync(ENV_FILE, envContent);
58
+ mkdirSync(join(DATA_DIR, "data", "files"), { recursive: true });
59
+
60
+ console.log(`
61
+ Initialized openclaw-bridge-hub at ${DATA_DIR}
62
+
63
+ Your API key (save this — clients need it to connect):
64
+
65
+ ${apiKey}
66
+
67
+ Config file: ${ENV_FILE}
68
+ Data dir: ${DATA_DIR}/data
69
+
70
+ Next steps:
71
+ openclaw-bridge-hub start # start the server
72
+ openclaw-bridge-hub install-service # auto-start on boot (Linux)
73
+ `);
74
+ }
75
+
76
+ function start() {
77
+ if (existsSync(ENV_FILE)) {
78
+ // Load env from config file
79
+ const content = readFileSync(ENV_FILE, "utf-8");
80
+ for (const line of content.split("\n")) {
81
+ const trimmed = line.trim();
82
+ if (!trimmed || trimmed.startsWith("#")) continue;
83
+ const eqIdx = trimmed.indexOf("=");
84
+ if (eqIdx > 0) {
85
+ const key = trimmed.slice(0, eqIdx).trim();
86
+ const val = trimmed.slice(eqIdx + 1).trim();
87
+ if (!process.env[key]) process.env[key] = val;
88
+ }
89
+ }
90
+ } else {
91
+ console.log("No config found. Run 'openclaw-bridge-hub init' first.");
92
+ process.exit(1);
93
+ }
94
+
95
+ // Import and start the server
96
+ import("./index.js");
97
+ }
98
+
99
+ function status() {
100
+ if (!existsSync(ENV_FILE)) {
101
+ console.log("Not initialized. Run: openclaw-bridge-hub init");
102
+ return;
103
+ }
104
+
105
+ const content = readFileSync(ENV_FILE, "utf-8");
106
+ const portMatch = content.match(/^PORT=(\d+)/m);
107
+ const port = portMatch ? portMatch[1] : "3080";
108
+
109
+ try {
110
+ execSync(`curl -sf http://localhost:${port}/api/v1/health`, { timeout: 5000 });
111
+ console.log(`FileRelay is running on port ${port}`);
112
+ } catch {
113
+ console.log(`FileRelay is NOT running (port ${port})`);
114
+ }
115
+ }
116
+
117
+ function installService() {
118
+ if (!IS_LINUX) {
119
+ console.log("systemd service is only supported on Linux.");
120
+ console.log("On other platforms, use pm2 or your system's service manager.");
121
+ return;
122
+ }
123
+
124
+ // Find the actual path of openclaw-bridge-hub bin
125
+ let binPath: string;
126
+ try {
127
+ binPath = execSync("which openclaw-bridge-hub", { encoding: "utf-8" }).trim();
128
+ } catch {
129
+ binPath = "/usr/local/bin/openclaw-bridge-hub";
130
+ }
131
+
132
+ const user = process.env.USER || "root";
133
+
134
+ const serviceContent = `[Unit]
135
+ Description=OpenClaw Bridge FileRelay
136
+ After=network.target
137
+
138
+ [Service]
139
+ Type=simple
140
+ User=${user}
141
+ Environment=BRIDGE_FILERELAY_DATA=${DATA_DIR}
142
+ ExecStart=${binPath} start
143
+ Restart=always
144
+ RestartSec=5
145
+
146
+ [Install]
147
+ WantedBy=multi-user.target
148
+ `;
149
+
150
+ const servicePath = "/etc/systemd/system/openclaw-bridge-hub.service";
151
+ try {
152
+ writeFileSync(servicePath, serviceContent);
153
+ execSync("systemctl daemon-reload");
154
+ execSync("systemctl enable openclaw-bridge-hub");
155
+ execSync("systemctl start openclaw-bridge-hub");
156
+ console.log(`
157
+ Service installed and started!
158
+
159
+ systemctl status openclaw-bridge-hub # check status
160
+ systemctl restart openclaw-bridge-hub # restart
161
+ journalctl -u openclaw-bridge-hub -f # view logs
162
+ `);
163
+ } catch (err) {
164
+ console.log(`Failed to install service. You may need sudo:\n sudo openclaw-bridge-hub install-service`);
165
+ // Write service file to data dir as fallback
166
+ const fallbackPath = join(DATA_DIR, "openclaw-bridge-hub.service");
167
+ writeFileSync(fallbackPath, serviceContent);
168
+ console.log(`\nService file saved to: ${fallbackPath}`);
169
+ console.log("Install manually:");
170
+ console.log(` sudo cp ${fallbackPath} /etc/systemd/system/`);
171
+ console.log(" sudo systemctl daemon-reload && sudo systemctl enable --now openclaw-bridge-hub");
172
+ }
173
+ }
174
+
175
+ function uninstallService() {
176
+ if (!IS_LINUX) {
177
+ console.log("systemd service is only supported on Linux.");
178
+ return;
179
+ }
180
+ try {
181
+ execSync("systemctl stop openclaw-bridge-hub");
182
+ execSync("systemctl disable openclaw-bridge-hub");
183
+ execSync("rm /etc/systemd/system/openclaw-bridge-hub.service");
184
+ execSync("systemctl daemon-reload");
185
+ console.log("Service removed.");
186
+ } catch {
187
+ console.log("Failed to remove service. Try with sudo.");
188
+ }
189
+ }
190
+
191
+ switch (command) {
192
+ case "init":
193
+ init();
194
+ break;
195
+ case "start":
196
+ start();
197
+ break;
198
+ case "status":
199
+ status();
200
+ break;
201
+ case "install-service":
202
+ installService();
203
+ break;
204
+ case "uninstall-service":
205
+ uninstallService();
206
+ break;
207
+ default:
208
+ printHelp();
209
+ }
package/src/config.ts ADDED
@@ -0,0 +1,10 @@
1
+ import "dotenv/config";
2
+
3
+ export const config = {
4
+ port: parseInt(process.env.PORT ?? "3080", 10),
5
+ apiKey: process.env.API_KEY ?? "",
6
+ dataDir: process.env.DATA_DIR ?? "./data",
7
+ cleanupIntervalMs: parseInt(process.env.CLEANUP_INTERVAL_MS ?? "3600000", 10),
8
+ fileTtlMs: parseInt(process.env.FILE_TTL_MS ?? "86400000", 10),
9
+ commandTtlMs: parseInt(process.env.COMMAND_TTL_MS ?? "3600000", 10),
10
+ };
package/src/db.ts ADDED
@@ -0,0 +1,48 @@
1
+ import Database from "better-sqlite3";
2
+ import { mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { config } from "./config.js";
5
+
6
+ mkdirSync(config.dataDir, { recursive: true });
7
+ mkdirSync(join(config.dataDir, "files"), { recursive: true });
8
+
9
+ const dbPath = join(config.dataDir, "relay.db");
10
+ export const db = new Database(dbPath);
11
+
12
+ db.pragma("journal_mode = WAL");
13
+
14
+ db.exec(`
15
+ CREATE TABLE IF NOT EXISTS files (
16
+ id TEXT PRIMARY KEY,
17
+ fromAgent TEXT NOT NULL,
18
+ toAgent TEXT NOT NULL,
19
+ filename TEXT NOT NULL,
20
+ diskPath TEXT NOT NULL,
21
+ size INTEGER NOT NULL DEFAULT 0,
22
+ metadata TEXT NOT NULL DEFAULT '{}',
23
+ status TEXT NOT NULL DEFAULT 'pending',
24
+ createdAt TEXT NOT NULL
25
+ );
26
+
27
+ CREATE TABLE IF NOT EXISTS commands (
28
+ id TEXT PRIMARY KEY,
29
+ fromAgent TEXT NOT NULL,
30
+ toAgent TEXT NOT NULL,
31
+ type TEXT NOT NULL,
32
+ payload TEXT NOT NULL DEFAULT '{}',
33
+ status TEXT NOT NULL DEFAULT 'queued',
34
+ response TEXT,
35
+ createdAt TEXT NOT NULL,
36
+ respondedAt TEXT
37
+ );
38
+
39
+ CREATE INDEX IF NOT EXISTS idx_files_toAgent_status ON files(toAgent, status);
40
+ CREATE INDEX IF NOT EXISTS idx_commands_toAgent_status ON commands(toAgent, status);
41
+
42
+ CREATE TABLE IF NOT EXISTS registry (
43
+ agentId TEXT PRIMARY KEY,
44
+ data TEXT NOT NULL DEFAULT '{}',
45
+ lastHeartbeat TEXT NOT NULL,
46
+ createdAt TEXT NOT NULL
47
+ );
48
+ `);
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ import Fastify from "fastify";
2
+ import { config } from "./config.js";
3
+ import "./db.js";
4
+ import { registerFileRoutes } from "./routes/files.js";
5
+ import { registerCommandRoutes } from "./routes/commands.js";
6
+ import { registerHealthRoutes } from "./routes/health.js";
7
+ import { registerRegistryRoutes } from "./routes/registry.js";
8
+ import { authMiddleware } from "./middleware/auth.js";
9
+ import { startCleanup } from "./cleanup.js";
10
+
11
+ const app = Fastify({ logger: true });
12
+
13
+ app.addHook("onRequest", authMiddleware);
14
+
15
+ registerFileRoutes(app);
16
+ registerCommandRoutes(app);
17
+ registerHealthRoutes(app);
18
+ registerRegistryRoutes(app);
19
+
20
+ const cleanupTimer = startCleanup();
21
+
22
+ const shutdown = async () => {
23
+ clearInterval(cleanupTimer);
24
+ await app.close();
25
+ process.exit(0);
26
+ };
27
+ process.on("SIGINT", shutdown);
28
+ process.on("SIGTERM", shutdown);
29
+
30
+ app.listen({ port: config.port, host: "0.0.0.0" }, (err, address) => {
31
+ if (err) {
32
+ console.error("Failed to start FileRelay:", err);
33
+ process.exit(1);
34
+ }
35
+ console.log(`Bridge FileRelay listening on ${address}`);
36
+ });
@@ -0,0 +1,13 @@
1
+ import type { FastifyRequest, FastifyReply } from "fastify";
2
+ import { config } from "../config.js";
3
+
4
+ export async function authMiddleware(
5
+ request: FastifyRequest,
6
+ reply: FastifyReply,
7
+ ): Promise<void> {
8
+ if (!config.apiKey) return;
9
+ const key = request.headers["x-api-key"];
10
+ if (key !== config.apiKey) {
11
+ reply.code(401).send({ error: "Invalid API key" });
12
+ }
13
+ }
@@ -0,0 +1,93 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import { v4 as uuid } from "uuid";
3
+ import { db } from "../db.js";
4
+
5
+ export function registerCommandRoutes(app: FastifyInstance): void {
6
+ app.post("/api/v1/commands/enqueue", async (request) => {
7
+ const body = request.body as {
8
+ fromAgent: string;
9
+ toAgent: string;
10
+ type: string;
11
+ payload: Record<string, unknown>;
12
+ };
13
+
14
+ const id = uuid();
15
+ const createdAt = new Date().toISOString();
16
+ db.prepare(
17
+ `INSERT INTO commands (id, fromAgent, toAgent, type, payload, status, createdAt)
18
+ VALUES (?, ?, ?, ?, ?, 'queued', ?)`,
19
+ ).run(id, body.fromAgent, body.toAgent, body.type, JSON.stringify(body.payload), createdAt);
20
+
21
+ return { id, status: "queued" };
22
+ });
23
+
24
+ app.get("/api/v1/commands/pending", async (request) => {
25
+ const { agent } = request.query as { agent: string };
26
+ const rows = db
27
+ .prepare(
28
+ `SELECT id, fromAgent, type, payload, createdAt
29
+ FROM commands WHERE toAgent = ? AND status = 'queued'
30
+ ORDER BY createdAt ASC`,
31
+ )
32
+ .all(agent) as Array<{
33
+ id: string;
34
+ fromAgent: string;
35
+ type: string;
36
+ payload: string;
37
+ createdAt: string;
38
+ }>;
39
+
40
+ return {
41
+ commands: rows.map((r) => ({
42
+ ...r,
43
+ payload: JSON.parse(r.payload),
44
+ })),
45
+ };
46
+ });
47
+
48
+ app.post("/api/v1/commands/respond/:id", async (request, reply) => {
49
+ const { id } = request.params as { id: string };
50
+ const body = request.body as {
51
+ status: "ok" | "error";
52
+ payload: Record<string, unknown>;
53
+ };
54
+
55
+ const existing = db.prepare(`SELECT id FROM commands WHERE id = ?`).get(id);
56
+ if (!existing) {
57
+ reply.code(404).send({ error: "Command not found" });
58
+ return;
59
+ }
60
+
61
+ db.prepare(
62
+ `UPDATE commands SET status = ?, response = ?, respondedAt = ? WHERE id = ?`,
63
+ ).run(
64
+ body.status === "ok" ? "responded" : "error",
65
+ JSON.stringify(body.payload),
66
+ new Date().toISOString(),
67
+ id,
68
+ );
69
+
70
+ return { status: "updated" };
71
+ });
72
+
73
+ app.get("/api/v1/commands/result/:id", async (request, reply) => {
74
+ const { id } = request.params as { id: string };
75
+ const row = db.prepare(`SELECT status, response FROM commands WHERE id = ?`).get(id) as
76
+ | { status: string; response: string | null }
77
+ | undefined;
78
+
79
+ if (!row) {
80
+ reply.code(404).send({ error: "Command not found" });
81
+ return;
82
+ }
83
+
84
+ if (row.status === "queued") {
85
+ return { status: "queued" };
86
+ }
87
+
88
+ return {
89
+ status: row.status === "responded" ? "ok" : "error",
90
+ payload: row.response ? JSON.parse(row.response) : null,
91
+ };
92
+ });
93
+ }
@@ -0,0 +1,103 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import { writeFileSync, readFileSync, unlinkSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { v4 as uuid } from "uuid";
5
+ import { db } from "../db.js";
6
+ import { config } from "../config.js";
7
+
8
+ export function registerFileRoutes(app: FastifyInstance): void {
9
+ app.post("/api/v1/files/upload", async (request) => {
10
+ const body = request.body as {
11
+ fromAgent: string;
12
+ toAgent: string;
13
+ filename: string;
14
+ content: string;
15
+ metadata?: Record<string, unknown>;
16
+ };
17
+
18
+ const id = uuid();
19
+ const diskPath = join(config.dataDir, "files", `${id}.bin`);
20
+ const buffer = Buffer.from(body.content, "base64");
21
+ writeFileSync(diskPath, buffer);
22
+
23
+ const createdAt = new Date().toISOString();
24
+ db.prepare(
25
+ `INSERT INTO files (id, fromAgent, toAgent, filename, diskPath, size, metadata, status, createdAt)
26
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?)`,
27
+ ).run(
28
+ id,
29
+ body.fromAgent,
30
+ body.toAgent,
31
+ body.filename,
32
+ diskPath,
33
+ buffer.length,
34
+ JSON.stringify(body.metadata ?? {}),
35
+ createdAt,
36
+ );
37
+
38
+ return { id, status: "pending", createdAt };
39
+ });
40
+
41
+ app.get("/api/v1/files/pending", async (request) => {
42
+ const { agent } = request.query as { agent: string };
43
+ const rows = db
44
+ .prepare(
45
+ `SELECT id, fromAgent, filename, size, metadata, createdAt
46
+ FROM files WHERE toAgent = ? AND status = 'pending'
47
+ ORDER BY createdAt ASC`,
48
+ )
49
+ .all(agent) as Array<{
50
+ id: string;
51
+ fromAgent: string;
52
+ filename: string;
53
+ size: number;
54
+ metadata: string;
55
+ createdAt: string;
56
+ }>;
57
+
58
+ return {
59
+ files: rows.map((r) => ({
60
+ ...r,
61
+ metadata: JSON.parse(r.metadata),
62
+ })),
63
+ };
64
+ });
65
+
66
+ app.get("/api/v1/files/download/:id", async (request, reply) => {
67
+ const { id } = request.params as { id: string };
68
+ const row = db.prepare(`SELECT diskPath FROM files WHERE id = ?`).get(id) as
69
+ | { diskPath: string }
70
+ | undefined;
71
+
72
+ if (!row) {
73
+ reply.code(404).send({ error: "File not found" });
74
+ return;
75
+ }
76
+
77
+ try {
78
+ const content = readFileSync(row.diskPath);
79
+ return { content: content.toString("base64") };
80
+ } catch {
81
+ reply.code(404).send({ error: "File data not found on disk" });
82
+ }
83
+ });
84
+
85
+ app.post("/api/v1/files/ack/:id", async (request, reply) => {
86
+ const { id } = request.params as { id: string };
87
+ const row = db.prepare(`SELECT diskPath FROM files WHERE id = ?`).get(id) as
88
+ | { diskPath: string }
89
+ | undefined;
90
+
91
+ if (!row) {
92
+ reply.code(404).send({ error: "File not found" });
93
+ return;
94
+ }
95
+
96
+ try {
97
+ unlinkSync(row.diskPath);
98
+ } catch {}
99
+ db.prepare(`UPDATE files SET status = 'acknowledged' WHERE id = ?`).run(id);
100
+
101
+ return { status: "acknowledged" };
102
+ });
103
+ }
@@ -0,0 +1,41 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import { db } from "../db.js";
3
+ import { statSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { config } from "../config.js";
6
+
7
+ export function registerHealthRoutes(app: FastifyInstance): void {
8
+ app.get("/api/v1/health", async () => {
9
+ return { status: "ok", timestamp: new Date().toISOString() };
10
+ });
11
+
12
+ app.get("/api/v1/stats", async () => {
13
+ const pendingFiles = (
14
+ db.prepare(`SELECT COUNT(*) as count FROM files WHERE status = 'pending'`).get() as {
15
+ count: number;
16
+ }
17
+ ).count;
18
+ const queuedCommands = (
19
+ db.prepare(`SELECT COUNT(*) as count FROM commands WHERE status = 'queued'`).get() as {
20
+ count: number;
21
+ }
22
+ ).count;
23
+
24
+ let diskUsageBytes = 0;
25
+ try {
26
+ const dbFile = join(config.dataDir, "relay.db");
27
+ try { diskUsageBytes += statSync(dbFile).size; } catch {}
28
+ const rows = db.prepare(`SELECT diskPath FROM files WHERE status = 'pending'`).all() as Array<{ diskPath: string }>;
29
+ for (const r of rows) {
30
+ try { diskUsageBytes += statSync(r.diskPath).size; } catch {}
31
+ }
32
+ } catch {}
33
+
34
+ return {
35
+ pendingFiles,
36
+ queuedCommands,
37
+ diskUsageBytes,
38
+ diskUsageMB: Math.round(diskUsageBytes / 1024 / 1024 * 100) / 100,
39
+ };
40
+ });
41
+ }
@@ -0,0 +1,87 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import { db } from "../db.js";
3
+
4
+ export function registerRegistryRoutes(app: FastifyInstance): void {
5
+ // Register or update a gateway entry
6
+ app.post("/api/v1/registry/register", async (request) => {
7
+ const body = request.body as Record<string, unknown>;
8
+ const agentId = body.agentId as string;
9
+ if (!agentId) return { error: "agentId is required" };
10
+
11
+ const now = new Date().toISOString();
12
+ const data = JSON.stringify(body);
13
+
14
+ db.prepare(
15
+ `INSERT INTO registry (agentId, data, lastHeartbeat, createdAt)
16
+ VALUES (?, ?, ?, ?)
17
+ ON CONFLICT(agentId) DO UPDATE SET data = ?, lastHeartbeat = ?`,
18
+ ).run(agentId, data, now, now, data, now);
19
+
20
+ return { status: "ok", agentId };
21
+ });
22
+
23
+ // Heartbeat — update lastHeartbeat and optionally update data
24
+ app.post("/api/v1/registry/heartbeat", async (request) => {
25
+ const body = request.body as Record<string, unknown>;
26
+ const agentId = body.agentId as string;
27
+ if (!agentId) return { error: "agentId is required" };
28
+
29
+ const now = new Date().toISOString();
30
+
31
+ if (Object.keys(body).length > 1) {
32
+ // Full data update
33
+ const data = JSON.stringify(body);
34
+ db.prepare(
35
+ `UPDATE registry SET data = ?, lastHeartbeat = ? WHERE agentId = ?`,
36
+ ).run(data, now, agentId);
37
+ } else {
38
+ // Heartbeat only
39
+ db.prepare(
40
+ `UPDATE registry SET lastHeartbeat = ? WHERE agentId = ?`,
41
+ ).run(now, agentId);
42
+ }
43
+
44
+ return { status: "ok", agentId, lastHeartbeat: now };
45
+ });
46
+
47
+ // Discover — list all registered gateways
48
+ app.get("/api/v1/registry/discover", async () => {
49
+ const rows = db
50
+ .prepare(`SELECT agentId, data, lastHeartbeat, createdAt FROM registry ORDER BY agentId`)
51
+ .all() as Array<{
52
+ agentId: string;
53
+ data: string;
54
+ lastHeartbeat: string;
55
+ createdAt: string;
56
+ }>;
57
+
58
+ return {
59
+ agents: rows.map((r) => ({
60
+ ...JSON.parse(r.data),
61
+ lastHeartbeat: r.lastHeartbeat,
62
+ })),
63
+ };
64
+ });
65
+
66
+ // Whois — get a specific gateway
67
+ app.get("/api/v1/registry/whois/:agentId", async (request, reply) => {
68
+ const { agentId } = request.params as { agentId: string };
69
+ const row = db
70
+ .prepare(`SELECT data, lastHeartbeat FROM registry WHERE agentId = ?`)
71
+ .get(agentId) as { data: string; lastHeartbeat: string } | undefined;
72
+
73
+ if (!row) {
74
+ reply.code(404).send({ error: `Agent "${agentId}" not found` });
75
+ return;
76
+ }
77
+
78
+ return { ...JSON.parse(row.data), lastHeartbeat: row.lastHeartbeat };
79
+ });
80
+
81
+ // Deregister — remove a gateway
82
+ app.delete("/api/v1/registry/:agentId", async (request) => {
83
+ const { agentId } = request.params as { agentId: string };
84
+ db.prepare(`DELETE FROM registry WHERE agentId = ?`).run(agentId);
85
+ return { status: "ok", agentId };
86
+ });
87
+ }
package/src/types.ts ADDED
@@ -0,0 +1,23 @@
1
+ export interface FileRecord {
2
+ id: string;
3
+ fromAgent: string;
4
+ toAgent: string;
5
+ filename: string;
6
+ diskPath: string;
7
+ size: number;
8
+ metadata: string;
9
+ status: "pending" | "acknowledged";
10
+ createdAt: string;
11
+ }
12
+
13
+ export interface CommandRecord {
14
+ id: string;
15
+ fromAgent: string;
16
+ toAgent: string;
17
+ type: string;
18
+ payload: string;
19
+ status: "queued" | "responded" | "error";
20
+ response: string | null;
21
+ createdAt: string;
22
+ respondedAt: string | null;
23
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "declaration": true
12
+ },
13
+ "include": ["src"]
14
+ }