symmetry-cli 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/.eslintrc.js ADDED
@@ -0,0 +1,15 @@
1
+ module.exports = {
2
+ parser: '@typescript-eslint/parser',
3
+ plugins: ['@typescript-eslint'],
4
+ extends: [
5
+ 'eslint:recommended',
6
+ 'plugin:@typescript-eslint/recommended',
7
+ ],
8
+ env: {
9
+ node: true,
10
+ jest: true,
11
+ },
12
+ rules: {
13
+ // You can add custom rules here
14
+ },
15
+ };
package/LICENCE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2024 Richard Macarthy (rjmacarthy)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,61 @@
1
+ import { SymmetryClient } from "../src/client";
2
+ import yaml from "js-yaml";
3
+
4
+ jest.mock("hyperswarm", () => {
5
+ return jest.fn().mockImplementation(() => ({
6
+ join: jest
7
+ .fn()
8
+ .mockReturnValue({ flushed: jest.fn().mockResolvedValue(undefined) }),
9
+ on: jest.fn(),
10
+ destroy: jest.fn().mockResolvedValue(undefined),
11
+ flush: jest.fn().mockResolvedValue(undefined),
12
+ }));
13
+ });
14
+
15
+ jest.mock("hypercore-crypto", () => ({
16
+ discoveryKey: jest.fn().mockReturnValue("test"),
17
+ keyPair: jest.fn().mockReturnValue({
18
+ publicKey: "test-public-key",
19
+ secretKey: "test-secret-key",
20
+ }),
21
+ sign: jest.fn(),
22
+ }));
23
+
24
+ jest.mock("fs", () => ({
25
+ readFileSync: jest.fn(),
26
+ writeFileSync: jest.fn(),
27
+ existsSync: jest.fn(),
28
+ }));
29
+
30
+ jest.mock("js-yaml", () => ({
31
+ load: jest.fn(),
32
+ }));
33
+
34
+
35
+ describe("Symmetry", () => {
36
+ let writer: SymmetryClient;
37
+ const mockConfig = {
38
+ path: "/test/path",
39
+ temperature: 1,
40
+ apiHostname: "test.api.com",
41
+ apiPort: 443,
42
+ apiPath: "/v1/chat",
43
+ apiProtocol: "https",
44
+ apiKey: "test-api-key",
45
+ apiProvider: "test-provider",
46
+ modelName: "test-model",
47
+ name: "test",
48
+ public: true,
49
+ serverKey: "test-server-key",
50
+ };
51
+
52
+ beforeEach(() => {
53
+ jest.clearAllMocks();
54
+ (yaml.load as jest.Mock).mockReturnValue(mockConfig);
55
+ writer = new SymmetryClient("mock-config.yaml");
56
+ });
57
+
58
+ test("init method sets up the writer correctly", async () => {
59
+ await writer.init();
60
+ });
61
+ });
package/dist/client.js ADDED
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SymmetryClient = void 0;
7
+ const node_stream_1 = require("node:stream");
8
+ const promises_1 = require("stream/promises");
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const hyperswarm_1 = __importDefault(require("hyperswarm"));
11
+ const hypercore_crypto_1 = __importDefault(require("hypercore-crypto"));
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ const config_1 = require("./config");
14
+ const utils_1 = require("./utils");
15
+ const logger_1 = require("./logger");
16
+ const constants_1 = require("./constants");
17
+ class SymmetryClient {
18
+ constructor(configPath) {
19
+ this._conversationIndex = 0;
20
+ this._discoveryKey = null;
21
+ this._isPublic = false;
22
+ this._providerSwarm = null;
23
+ this._serverPeer = null;
24
+ this._challenge = null;
25
+ logger_1.logger.info(`🔗 Initializing client using config file: ${configPath}`);
26
+ this._config = new config_1.ConfigManager(configPath);
27
+ this._isPublic = this._config.get("public");
28
+ }
29
+ async init() {
30
+ this._providerSwarm = new hyperswarm_1.default();
31
+ const keyPair = hypercore_crypto_1.default.keyPair();
32
+ this._discoveryKey = hypercore_crypto_1.default.discoveryKey(keyPair.publicKey);
33
+ const discovery = this._providerSwarm.join(this._discoveryKey, {
34
+ client: true,
35
+ server: false,
36
+ });
37
+ await discovery.flushed();
38
+ this._providerSwarm.on("error", (err) => {
39
+ logger_1.logger.error(chalk_1.default.red("🚨 Swarm Error:"), err);
40
+ });
41
+ this._providerSwarm.on("connection", (peer) => {
42
+ var _a, _b;
43
+ (_a = this._serverPeer) === null || _a === void 0 ? void 0 : _a.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.conectionSize, {
44
+ connections: (_b = this._providerSwarm) === null || _b === void 0 ? void 0 : _b.connections.size,
45
+ }));
46
+ logger_1.logger.info(`⚡️ New connection from peer: ${peer.rawStream.remoteHost}`);
47
+ this.listeners(peer);
48
+ });
49
+ logger_1.logger.info(`📁 Symmetry client initialized.`);
50
+ logger_1.logger.info(`🔑 Discovery key: ${this._discoveryKey.toString("hex")}`);
51
+ if (this._isPublic) {
52
+ logger_1.logger.info(chalk_1.default.white(`🔑 Server key: ${this._config.get("serverKey")}`));
53
+ logger_1.logger.info(chalk_1.default.white("🔗 Joining server, please wait."));
54
+ await this.joinServer();
55
+ }
56
+ }
57
+ async joinServer() {
58
+ const serverSwarm = new hyperswarm_1.default();
59
+ const serverKey = Buffer.from(this._config.get("serverKey"));
60
+ serverSwarm.join(hypercore_crypto_1.default.discoveryKey(serverKey), {
61
+ client: true,
62
+ server: false,
63
+ });
64
+ serverSwarm.flush();
65
+ serverSwarm.on("connection", (peer) => {
66
+ this._serverPeer = peer;
67
+ logger_1.logger.info(chalk_1.default.green("🔗 Connected to server."));
68
+ this._challenge = hypercore_crypto_1.default.randomBytes(32);
69
+ this._serverPeer.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.challenge, {
70
+ challenge: this._challenge,
71
+ }));
72
+ this._serverPeer.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.join, {
73
+ ...this._config.getAll(),
74
+ discoveryKey: this._discoveryKey,
75
+ }));
76
+ this._serverPeer.on("data", async (buffer) => {
77
+ if (!buffer)
78
+ return;
79
+ const data = (0, utils_1.safeParseJson)(buffer.toString());
80
+ if (data && data.key) {
81
+ switch (data.key) {
82
+ case constants_1.serverMessageKeys.challenge:
83
+ this.handleServerHandshake(data.data);
84
+ break;
85
+ }
86
+ }
87
+ });
88
+ });
89
+ }
90
+ getServerPublicKey(serverKeyHex) {
91
+ const publicKey = Buffer.from(serverKeyHex, "hex");
92
+ if (publicKey.length !== 32) {
93
+ throw new Error(`Expected a 32-byte public key, but got ${publicKey.length} bytes`);
94
+ }
95
+ return publicKey;
96
+ }
97
+ handleServerHandshake(data) {
98
+ if (!this._challenge) {
99
+ console.log("No challenge set. Cannot verify.");
100
+ return;
101
+ }
102
+ const serverKeyHex = this._config.get("serverKey");
103
+ try {
104
+ const publicKey = this.getServerPublicKey(serverKeyHex);
105
+ const signatureBuffer = Buffer.from(data.signature.data, "base64");
106
+ const verified = hypercore_crypto_1.default.verify(this._challenge, signatureBuffer, publicKey);
107
+ if (verified) {
108
+ logger_1.logger.info(chalk_1.default.greenBright(`✅ Verification handshake successful.`));
109
+ }
110
+ else {
111
+ logger_1.logger.error(`❌ Verification failed!`);
112
+ }
113
+ }
114
+ catch (error) {
115
+ console.error("Error during verification:", error);
116
+ }
117
+ }
118
+ heartbeat(peer) {
119
+ peer.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.heartbeat, {
120
+ discoveryKey: this._discoveryKey,
121
+ }));
122
+ }
123
+ listeners(peer) {
124
+ peer.on("close", () => {
125
+ logger_1.logger.info(`🔌 Peer ${peer.rawStream.remoteHost} disconnected.`);
126
+ });
127
+ peer.on("data", async (buffer) => {
128
+ if (!buffer)
129
+ return;
130
+ const data = (0, utils_1.safeParseJson)(buffer.toString());
131
+ if (data && data.key) {
132
+ switch (data.key) {
133
+ case constants_1.serverMessageKeys.newConversation:
134
+ this._conversationIndex = this._conversationIndex + 1;
135
+ break;
136
+ case constants_1.serverMessageKeys.inference:
137
+ logger_1.logger.info(`📦 Inference message received from ${peer.rawStream.remoteHost}`);
138
+ await this.handleInference(data, peer);
139
+ break;
140
+ }
141
+ }
142
+ });
143
+ }
144
+ async handleInference(data, peer) {
145
+ const messages = data === null || data === void 0 ? void 0 : data.data;
146
+ if (!messages || !messages.length)
147
+ return;
148
+ const req = this.buildStreamRequest(messages);
149
+ if (!req)
150
+ return;
151
+ const { requestOptions, requestBody } = req;
152
+ const { protocol, hostname, port, path, method, headers } = requestOptions;
153
+ const url = `${protocol}://${hostname}:${port}${path}`;
154
+ try {
155
+ const response = await fetch(url, {
156
+ method,
157
+ headers,
158
+ body: JSON.stringify(requestBody),
159
+ });
160
+ if (!response.ok) {
161
+ throw new Error(`Server responded with status code: ${response.status}`);
162
+ }
163
+ if (!response.body) {
164
+ throw new Error("Failed to get a ReadableStream from the response");
165
+ }
166
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
167
+ const responseStream = node_stream_1.Readable.fromWeb(response.body);
168
+ const peerStream = new node_stream_1.PassThrough();
169
+ responseStream.pipe(peerStream);
170
+ let completion = "";
171
+ const provider = this._config.get("apiProvider");
172
+ const peerPipeline = (0, promises_1.pipeline)(peerStream, async function (source) {
173
+ for await (const chunk of source) {
174
+ if (peer.writable) {
175
+ completion += (0, utils_1.getChatDataFromProvider)(provider, (0, utils_1.safeParseStreamResponse)(chunk.toString()));
176
+ if (!peer.write(chunk)) {
177
+ await new Promise((resolve) => peer.once("drain", resolve));
178
+ }
179
+ }
180
+ else {
181
+ break;
182
+ }
183
+ }
184
+ });
185
+ await Promise.resolve(peerPipeline);
186
+ if (this._config.get("dataCollectionEnabled")) {
187
+ this.saveCompletion(completion, peer, messages);
188
+ }
189
+ }
190
+ catch (error) {
191
+ let errorMessage = "An error occurred during inference";
192
+ if (error instanceof Error)
193
+ errorMessage = error.message;
194
+ logger_1.logger.error(`🚨 ${errorMessage}`);
195
+ }
196
+ }
197
+ async saveCompletion(completion, peer, messages) {
198
+ node_fs_1.default.writeFile(`${this._config.get("path")}/${peer.publicKey.toString("hex")}-${this._conversationIndex}.json`, JSON.stringify([
199
+ ...messages,
200
+ {
201
+ role: "assistant",
202
+ content: completion,
203
+ },
204
+ ]), () => {
205
+ logger_1.logger.info(`📝 Completion saved to file`);
206
+ });
207
+ }
208
+ buildStreamRequest(messages) {
209
+ const requestOptions = {
210
+ hostname: this._config.get("apiHostname"),
211
+ port: Number(this._config.get("apiPort")),
212
+ path: this._config.get("apiPath"),
213
+ protocol: this._config.get("apiProtocol"),
214
+ method: "POST",
215
+ headers: {
216
+ "Content-Type": "application/json",
217
+ Authorization: `Bearer ${this._config.get("apiKey")}`,
218
+ },
219
+ };
220
+ const requestBody = {
221
+ model: this._config.get("modelName"),
222
+ messages: messages || undefined,
223
+ temperature: 1, // TODO: Make this configurable
224
+ stream: true,
225
+ };
226
+ return { requestOptions, requestBody };
227
+ }
228
+ }
229
+ exports.SymmetryClient = SymmetryClient;
230
+ exports.default = SymmetryClient;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ConfigManager = void 0;
7
+ exports.createConfigManager = createConfigManager;
8
+ exports.createConfigManagerClient = createConfigManagerClient;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const js_yaml_1 = __importDefault(require("js-yaml"));
11
+ class ConfigManager {
12
+ constructor(configPath) {
13
+ const configFile = fs_1.default.readFileSync(configPath, "utf8");
14
+ const loadedConfig = js_yaml_1.default.load(configFile);
15
+ this.config = {
16
+ ...loadedConfig,
17
+ type: "client"
18
+ };
19
+ this.validate();
20
+ }
21
+ validate() {
22
+ this.validateClientConfig(this.config);
23
+ }
24
+ validateClientConfig(config) {
25
+ const requiredFields = [
26
+ "apiHostname",
27
+ "apiPath",
28
+ "apiPort",
29
+ "apiProtocol",
30
+ "apiProvider",
31
+ "modelName",
32
+ "path",
33
+ "public",
34
+ "serverKey",
35
+ ];
36
+ for (const field of requiredFields) {
37
+ if (!(field in config)) {
38
+ throw new Error(`Missing required field in client configuration: ${field}`);
39
+ }
40
+ }
41
+ if (typeof config.public !== "boolean") {
42
+ throw new Error('The "public" field in client configuration must be a boolean');
43
+ }
44
+ }
45
+ get(key) {
46
+ return this.config[key];
47
+ }
48
+ isClientConfig() {
49
+ return this.config.type === "client";
50
+ }
51
+ }
52
+ exports.ConfigManager = ConfigManager;
53
+ function createConfigManager(configPath) {
54
+ return new ConfigManager(configPath);
55
+ }
56
+ function createConfigManagerClient(configPath) {
57
+ return new ConfigManager(configPath);
58
+ }
package/dist/config.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ConfigManager = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const js_yaml_1 = __importDefault(require("js-yaml"));
9
+ class ConfigManager {
10
+ constructor(configPath) {
11
+ const configFile = fs_1.default.readFileSync(configPath, "utf8");
12
+ const config = js_yaml_1.default.load(configFile);
13
+ this.config = config;
14
+ this.validate();
15
+ }
16
+ getAll() {
17
+ return this.config;
18
+ }
19
+ validate() {
20
+ const requiredFields = [
21
+ "apiHostname",
22
+ "apiPath",
23
+ "apiPort",
24
+ "apiProtocol",
25
+ "apiProvider",
26
+ "modelName",
27
+ "path",
28
+ "public",
29
+ "serverKey",
30
+ ];
31
+ for (const field of requiredFields) {
32
+ if (!(field in this.config)) {
33
+ throw new Error(`Missing required field in client configuration: ${field}`);
34
+ }
35
+ }
36
+ if (typeof this.config.public !== "boolean") {
37
+ throw new Error('The "public" field in client configuration must be a boolean');
38
+ }
39
+ }
40
+ get(key) {
41
+ return this.config[key];
42
+ }
43
+ }
44
+ exports.ConfigManager = ConfigManager;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.apiProviders = exports.serverMessageKeys = exports.NORMALIZE_REGEX = void 0;
4
+ exports.NORMALIZE_REGEX = /\s*\r?\n|\r/g;
5
+ exports.serverMessageKeys = {
6
+ challenge: "challenge",
7
+ conectionSize: "conectionSize",
8
+ heartbeat: "heartbeat",
9
+ inference: "inference",
10
+ inferenceEnded: "inferenceEnded",
11
+ join: "join",
12
+ joinAck: "joinAck",
13
+ leave: "leave",
14
+ newConversation: "newConversation",
15
+ ping: "ping",
16
+ pong: "pong",
17
+ providerDetails: "providerDetails",
18
+ reportCompletion: "reportCompletion",
19
+ requestProvider: "requestProvider",
20
+ sessionValid: "sessionValid",
21
+ verifySession: "verifySession",
22
+ };
23
+ exports.apiProviders = {
24
+ LiteLLM: 'litellm',
25
+ LlamaCpp: 'llamacpp',
26
+ LMStudio: 'lmstudio',
27
+ Ollama: 'ollama',
28
+ Oobabooga: 'oobabooga',
29
+ OpenWebUI: 'openwebui',
30
+ };
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.database = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const sqlite3_1 = __importDefault(require("sqlite3"));
9
+ exports.database = new sqlite3_1.default.Database(path_1.default.join(__dirname, "../sqlite.db"));
package/dist/logger.js ADDED
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.logger = exports.Logger = exports.LogLevel = void 0;
7
+ /* eslint-disable @typescript-eslint/no-explicit-any */
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ var LogLevel;
10
+ (function (LogLevel) {
11
+ LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
12
+ LogLevel[LogLevel["ERROR"] = 1] = "ERROR";
13
+ LogLevel[LogLevel["INFO"] = 2] = "INFO";
14
+ LogLevel[LogLevel["WARNING"] = 3] = "WARNING";
15
+ })(LogLevel || (exports.LogLevel = LogLevel = {}));
16
+ class Logger {
17
+ constructor() {
18
+ this.logLevel = LogLevel.INFO;
19
+ }
20
+ static getInstance() {
21
+ if (!Logger.instance) {
22
+ Logger.instance = new Logger();
23
+ }
24
+ return Logger.instance;
25
+ }
26
+ setLogLevel(level) {
27
+ this.logLevel = level;
28
+ }
29
+ info(message, ...args) {
30
+ if (this.logLevel <= LogLevel.INFO) {
31
+ console.log(chalk_1.default.blue("ℹ️ INFO:"), message, ...args);
32
+ }
33
+ }
34
+ warning(message, ...args) {
35
+ console.log(chalk_1.default.yellow("⚠️ WARNING:"), message, ...args);
36
+ }
37
+ error(message, ...args) {
38
+ console.error(chalk_1.default.red("❌ ERROR:"), message, ...args);
39
+ }
40
+ debug(message, ...args) {
41
+ console.log(chalk_1.default.gray("🐛 DEBUG:"), message, ...args);
42
+ }
43
+ }
44
+ exports.Logger = Logger;
45
+ exports.logger = Logger.getInstance();
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PeerRepository = void 0;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const database_1 = require("./database");
9
+ const logger_1 = require("./logger");
10
+ class PeerRepository {
11
+ constructor() {
12
+ this.db = database_1.database;
13
+ }
14
+ upsert(message) {
15
+ return new Promise((resolve, reject) => {
16
+ this.db.run(`
17
+ INSERT OR REPLACE INTO peers (
18
+ key, discovery_key, gpu_memory, model_name, public, server_key, last_seen, online
19
+ ) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, TRUE)
20
+ `, [
21
+ message.key,
22
+ message.discoveryKey,
23
+ message.config.gpuMemory,
24
+ message.config.modelName,
25
+ message.config.public,
26
+ message.config.serverKey,
27
+ ], function (err) {
28
+ if (err) {
29
+ reject(err);
30
+ }
31
+ else {
32
+ resolve(this.lastID);
33
+ }
34
+ });
35
+ });
36
+ }
37
+ getByDiscoveryKey(discoveryKey) {
38
+ return new Promise((resolve, reject) => {
39
+ this.db.get("SELECT * FROM peers WHERE discovery_key = ?", [discoveryKey], (err, row) => {
40
+ if (err) {
41
+ reject(err);
42
+ }
43
+ else {
44
+ resolve(row);
45
+ }
46
+ });
47
+ });
48
+ }
49
+ getPeer(randomPeerRequest) {
50
+ return new Promise((resolve, reject) => {
51
+ const { modelName } = randomPeerRequest;
52
+ this.db.get(`SELECT * FROM peers WHERE model_name = ? ORDER BY RANDOM() LIMIT 1`, [modelName], (err, row) => {
53
+ if (err) {
54
+ reject(err);
55
+ }
56
+ else {
57
+ resolve(row);
58
+ }
59
+ });
60
+ });
61
+ }
62
+ updateLastSeen(peerKey) {
63
+ return new Promise((resolve, reject) => {
64
+ this.db.run("UPDATE peers SET last_seen = ?, online = FALSE WHERE key = ?", [new Date().toISOString(), peerKey], function (err) {
65
+ if (err) {
66
+ console.error(chalk_1.default.red("❌ Error updating peer last seen in database:"), err);
67
+ reject(err);
68
+ }
69
+ else {
70
+ if (this.changes > 0) {
71
+ logger_1.logger.info(chalk_1.default.yellow("🕒 Peer disconnected"));
72
+ }
73
+ else {
74
+ logger_1.logger.info(chalk_1.default.yellow("⚠️ Peer not found in database"));
75
+ }
76
+ resolve(this.changes);
77
+ }
78
+ });
79
+ });
80
+ }
81
+ async getActivePeerCount() {
82
+ return new Promise((resolve, reject) => {
83
+ this.db.get("SELECT COUNT(*) as count FROM peers WHERE online = TRUE", (err, row) => {
84
+ if (err) {
85
+ reject(err);
86
+ }
87
+ else {
88
+ resolve(row.count);
89
+ }
90
+ });
91
+ });
92
+ }
93
+ async getActiveModelCount() {
94
+ return new Promise((resolve, reject) => {
95
+ this.db.get("SELECT COUNT(DISTINCT model_name) as count FROM peers WHERE online = TRUE", (err, row) => {
96
+ if (err) {
97
+ reject(err);
98
+ }
99
+ else {
100
+ resolve(row.count);
101
+ }
102
+ });
103
+ });
104
+ }
105
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
+ updateStats(peerKey, data) {
107
+ // TODO: Update stats in database
108
+ logger_1.logger.info(peerKey, data);
109
+ }
110
+ }
111
+ exports.PeerRepository = PeerRepository;
112
+ module.exports = {
113
+ PeerRepository,
114
+ };