symmetry-cli 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.eslintrc.js +15 -0
- package/LICENCE +20 -0
- package/__test__/cli.test.ts +61 -0
- package/dist/client.js +230 -0
- package/dist/config-manager.js +58 -0
- package/dist/config.js +44 -0
- package/dist/constants.js +30 -0
- package/dist/database.js +9 -0
- package/dist/logger.js +45 -0
- package/dist/peer-repository.js +114 -0
- package/dist/provider.js +239 -0
- package/dist/server.js +143 -0
- package/dist/session-manager.js +68 -0
- package/dist/session-repository.js +127 -0
- package/dist/symmetry.js +20 -0
- package/dist/types.js +2 -0
- package/dist/utils.js +53 -0
- package/dist/websocket-server.js +52 -0
- package/global.d.ts +51 -0
- package/install.ps1 +88 -0
- package/jest.config.js +7 -0
- package/package.json +43 -0
- package/readme.md +167 -0
- package/src/config.ts +51 -0
- package/src/constants.ts +29 -0
- package/src/logger.ts +47 -0
- package/src/provider.ts +322 -0
- package/src/symmetry.ts +23 -0
- package/src/types.ts +242 -0
- package/src/utils.ts +52 -0
- package/tsconfig.json +28 -0
package/dist/provider.js
ADDED
@@ -0,0 +1,239 @@
|
|
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.SymmetryProvider = 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 SymmetryProvider {
|
18
|
+
constructor(configPath) {
|
19
|
+
this._challenge = null;
|
20
|
+
this._conversationIndex = 0;
|
21
|
+
this._discoveryKey = null;
|
22
|
+
this._isPublic = false;
|
23
|
+
this._providerConnections = 0;
|
24
|
+
this._providerSwarm = null;
|
25
|
+
this._serverPeer = null;
|
26
|
+
logger_1.logger.info(`🔗 Initializing client using config file: ${configPath}`);
|
27
|
+
this._config = new config_1.ConfigManager(configPath);
|
28
|
+
this._isPublic = this._config.get("public");
|
29
|
+
}
|
30
|
+
async init() {
|
31
|
+
this._providerSwarm = new hyperswarm_1.default({
|
32
|
+
maxConnections: this._config.get("maxConnections"),
|
33
|
+
});
|
34
|
+
const keyPair = hypercore_crypto_1.default.keyPair(Buffer.alloc(32).fill(this._config.get("name")));
|
35
|
+
this._discoveryKey = hypercore_crypto_1.default.discoveryKey(keyPair.publicKey);
|
36
|
+
const discovery = this._providerSwarm.join(this._discoveryKey, {
|
37
|
+
server: true,
|
38
|
+
client: true,
|
39
|
+
});
|
40
|
+
await discovery.flushed();
|
41
|
+
this._providerSwarm.on("error", (err) => {
|
42
|
+
logger_1.logger.error(chalk_1.default.red("🚨 Swarm Error:"), err);
|
43
|
+
});
|
44
|
+
this._providerSwarm.on("connection", (peer) => {
|
45
|
+
logger_1.logger.info(`⚡️ New connection from peer: ${peer.rawStream.remoteHost}`);
|
46
|
+
this.listeners(peer);
|
47
|
+
});
|
48
|
+
logger_1.logger.info(`📁 Symmetry client initialized.`);
|
49
|
+
logger_1.logger.info(`🔑 Discovery key: ${this._discoveryKey.toString("hex")}`);
|
50
|
+
if (this._isPublic) {
|
51
|
+
logger_1.logger.info(chalk_1.default.white(`🔑 Server key: ${this._config.get("serverKey")}`));
|
52
|
+
logger_1.logger.info(chalk_1.default.white("🔗 Joining server, please wait."));
|
53
|
+
await this.joinServer();
|
54
|
+
}
|
55
|
+
process.on("SIGINT", async () => {
|
56
|
+
var _a;
|
57
|
+
await ((_a = this._providerSwarm) === null || _a === void 0 ? void 0 : _a.destroy());
|
58
|
+
process.exit(0);
|
59
|
+
});
|
60
|
+
process.on("uncaughtException", (err) => {
|
61
|
+
if (err.message === "connection reset by peer") {
|
62
|
+
this._providerConnections = Math.max(0, this._providerConnections - 1);
|
63
|
+
}
|
64
|
+
});
|
65
|
+
}
|
66
|
+
async joinServer() {
|
67
|
+
const serverSwarm = new hyperswarm_1.default();
|
68
|
+
const serverKey = Buffer.from(this._config.get("serverKey"));
|
69
|
+
serverSwarm.join(hypercore_crypto_1.default.discoveryKey(serverKey), {
|
70
|
+
client: true,
|
71
|
+
server: false,
|
72
|
+
});
|
73
|
+
serverSwarm.flush();
|
74
|
+
serverSwarm.on("connection", (peer) => {
|
75
|
+
var _a;
|
76
|
+
this._serverPeer = peer;
|
77
|
+
logger_1.logger.info(chalk_1.default.green("🔗 Connected to server."));
|
78
|
+
this._challenge = hypercore_crypto_1.default.randomBytes(32);
|
79
|
+
this._serverPeer.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.challenge, {
|
80
|
+
challenge: this._challenge,
|
81
|
+
}));
|
82
|
+
this._serverPeer.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.join, {
|
83
|
+
...this._config.getAll(),
|
84
|
+
discoveryKey: (_a = this._discoveryKey) === null || _a === void 0 ? void 0 : _a.toString("hex"),
|
85
|
+
}));
|
86
|
+
this._serverPeer.on("data", async (buffer) => {
|
87
|
+
var _a;
|
88
|
+
if (!buffer)
|
89
|
+
return;
|
90
|
+
const data = (0, utils_1.safeParseJson)(buffer.toString());
|
91
|
+
if (data && data.key) {
|
92
|
+
switch (data.key) {
|
93
|
+
case constants_1.serverMessageKeys.challenge:
|
94
|
+
this.handleServerVerification(data.data);
|
95
|
+
break;
|
96
|
+
case constants_1.serverMessageKeys.ping:
|
97
|
+
(_a = this._serverPeer) === null || _a === void 0 ? void 0 : _a.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.pong));
|
98
|
+
break;
|
99
|
+
}
|
100
|
+
}
|
101
|
+
});
|
102
|
+
});
|
103
|
+
}
|
104
|
+
getServerPublicKey(serverKeyHex) {
|
105
|
+
const publicKey = Buffer.from(serverKeyHex, "hex");
|
106
|
+
if (publicKey.length !== 32) {
|
107
|
+
throw new Error(`Expected a 32-byte public key, but got ${publicKey.length} bytes`);
|
108
|
+
}
|
109
|
+
return publicKey;
|
110
|
+
}
|
111
|
+
handleServerVerification(data) {
|
112
|
+
if (!this._challenge) {
|
113
|
+
console.log("No challenge set. Cannot verify.");
|
114
|
+
return;
|
115
|
+
}
|
116
|
+
const serverKeyHex = this._config.get("serverKey");
|
117
|
+
try {
|
118
|
+
const publicKey = this.getServerPublicKey(serverKeyHex);
|
119
|
+
const signatureBuffer = Buffer.from(data.signature.data, "base64");
|
120
|
+
const verified = hypercore_crypto_1.default.verify(this._challenge, signatureBuffer, publicKey);
|
121
|
+
if (verified) {
|
122
|
+
logger_1.logger.info(chalk_1.default.greenBright(`✅ Verification successful.`));
|
123
|
+
}
|
124
|
+
else {
|
125
|
+
logger_1.logger.error(`❌ Verification failed!`);
|
126
|
+
}
|
127
|
+
}
|
128
|
+
catch (error) {
|
129
|
+
console.error("Error during verification:", error);
|
130
|
+
}
|
131
|
+
}
|
132
|
+
listeners(peer) {
|
133
|
+
peer.on("data", async (buffer) => {
|
134
|
+
if (!buffer)
|
135
|
+
return;
|
136
|
+
const data = (0, utils_1.safeParseJson)(buffer.toString());
|
137
|
+
if (data && data.key) {
|
138
|
+
switch (data.key) {
|
139
|
+
case constants_1.serverMessageKeys.newConversation:
|
140
|
+
this._conversationIndex = this._conversationIndex + 1;
|
141
|
+
break;
|
142
|
+
case constants_1.serverMessageKeys.inference:
|
143
|
+
logger_1.logger.info(`📦 Inference message received from ${peer.rawStream.remoteHost}`);
|
144
|
+
await this.handleInferenceRequest(data, peer);
|
145
|
+
break;
|
146
|
+
}
|
147
|
+
}
|
148
|
+
});
|
149
|
+
}
|
150
|
+
async handleInferenceRequest(data, peer) {
|
151
|
+
const emitterKey = data.data.key;
|
152
|
+
const req = this.buildStreamRequest(data === null || data === void 0 ? void 0 : data.data.messages);
|
153
|
+
if (!req)
|
154
|
+
return;
|
155
|
+
const { requestOptions, requestBody } = req;
|
156
|
+
const { protocol, hostname, port, path, method, headers } = requestOptions;
|
157
|
+
const url = `${protocol}://${hostname}:${port}${path}`;
|
158
|
+
try {
|
159
|
+
const response = await fetch(url, {
|
160
|
+
method,
|
161
|
+
headers,
|
162
|
+
body: JSON.stringify(requestBody),
|
163
|
+
});
|
164
|
+
if (!response.ok) {
|
165
|
+
throw new Error(`Server responded with status code: ${response.status}`);
|
166
|
+
}
|
167
|
+
if (!response.body) {
|
168
|
+
throw new Error("Failed to get a ReadableStream from the response");
|
169
|
+
}
|
170
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
171
|
+
const responseStream = node_stream_1.Readable.fromWeb(response.body);
|
172
|
+
const peerStream = new node_stream_1.PassThrough();
|
173
|
+
responseStream.pipe(peerStream);
|
174
|
+
let completion = "";
|
175
|
+
const provider = this._config.get("apiProvider");
|
176
|
+
peer.write(JSON.stringify({
|
177
|
+
symmetryEmitterKey: emitterKey,
|
178
|
+
}));
|
179
|
+
const peerPipeline = (0, promises_1.pipeline)(peerStream, async function (source) {
|
180
|
+
for await (const chunk of source) {
|
181
|
+
if (peer.writable) {
|
182
|
+
completion += (0, utils_1.getChatDataFromProvider)(provider, (0, utils_1.safeParseStreamResponse)(chunk.toString()));
|
183
|
+
const write = peer.write(chunk);
|
184
|
+
if (!write) {
|
185
|
+
await new Promise((resolve) => peer.once("drain", resolve));
|
186
|
+
}
|
187
|
+
}
|
188
|
+
else {
|
189
|
+
break;
|
190
|
+
}
|
191
|
+
}
|
192
|
+
});
|
193
|
+
await Promise.resolve(peerPipeline);
|
194
|
+
peer.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.inferenceEnded, data === null || data === void 0 ? void 0 : data.data.key));
|
195
|
+
if (this._config.get("dataCollectionEnabled") &&
|
196
|
+
data.data.key === constants_1.serverMessageKeys.inference) {
|
197
|
+
this.saveCompletion(completion, peer, data.data.messages);
|
198
|
+
}
|
199
|
+
}
|
200
|
+
catch (error) {
|
201
|
+
let errorMessage = "An error occurred during inference";
|
202
|
+
if (error instanceof Error)
|
203
|
+
errorMessage = error.message;
|
204
|
+
logger_1.logger.error(`🚨 ${errorMessage}`);
|
205
|
+
}
|
206
|
+
}
|
207
|
+
async saveCompletion(completion, peer, messages) {
|
208
|
+
node_fs_1.default.writeFile(`${this._config.get("path")}/${peer.publicKey.toString("hex")}-${this._conversationIndex}.json`, JSON.stringify([
|
209
|
+
...messages,
|
210
|
+
{
|
211
|
+
role: "assistant",
|
212
|
+
content: completion,
|
213
|
+
},
|
214
|
+
]), () => {
|
215
|
+
logger_1.logger.info(`📝 Completion saved to file`);
|
216
|
+
});
|
217
|
+
}
|
218
|
+
buildStreamRequest(messages) {
|
219
|
+
const requestOptions = {
|
220
|
+
hostname: this._config.get("apiHostname"),
|
221
|
+
port: Number(this._config.get("apiPort")),
|
222
|
+
path: this._config.get("apiPath"),
|
223
|
+
protocol: this._config.get("apiProtocol"),
|
224
|
+
method: "POST",
|
225
|
+
headers: {
|
226
|
+
"Content-Type": "application/json",
|
227
|
+
Authorization: `Bearer ${this._config.get("apiKey")}`,
|
228
|
+
},
|
229
|
+
};
|
230
|
+
const requestBody = {
|
231
|
+
model: this._config.get("modelName"),
|
232
|
+
messages: messages || undefined,
|
233
|
+
stream: true,
|
234
|
+
};
|
235
|
+
return { requestOptions, requestBody };
|
236
|
+
}
|
237
|
+
}
|
238
|
+
exports.SymmetryProvider = SymmetryProvider;
|
239
|
+
exports.default = SymmetryProvider;
|
package/dist/server.js
ADDED
@@ -0,0 +1,143 @@
|
|
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.SymmetryServer = void 0;
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
8
|
+
const corestore_1 = __importDefault(require("corestore"));
|
9
|
+
const hyperdrive_1 = __importDefault(require("hyperdrive"));
|
10
|
+
const hyperswarm_1 = __importDefault(require("hyperswarm"));
|
11
|
+
const path_1 = __importDefault(require("path"));
|
12
|
+
const config_manager_1 = require("./config-manager");
|
13
|
+
const utils_1 = require("./utils");
|
14
|
+
const logger_1 = require("./logger");
|
15
|
+
const peer_repository_1 = require("./peer-repository");
|
16
|
+
const constants_1 = require("./constants");
|
17
|
+
const session_manager_1 = require("./session-manager");
|
18
|
+
const session_repository_1 = require("./session-repository");
|
19
|
+
const websocket_server_1 = require("./websocket-server");
|
20
|
+
class SymmetryServer {
|
21
|
+
constructor(configPath) {
|
22
|
+
logger_1.logger.info(`🔗 Initializing server using config file: ${configPath}`);
|
23
|
+
this._config = (0, config_manager_1.createConfigManager)(configPath, false);
|
24
|
+
if (!this._config.isServerConfig()) {
|
25
|
+
throw new Error("Invalid configuration for server");
|
26
|
+
}
|
27
|
+
this._peerRepository = new peer_repository_1.PeerRepository();
|
28
|
+
this._sessionRepository = new session_repository_1.SessionRepository();
|
29
|
+
this._sessionManager = new session_manager_1.SessionManager(this._sessionRepository, 5);
|
30
|
+
}
|
31
|
+
async init() {
|
32
|
+
var _a, _b;
|
33
|
+
const corePath = path_1.default.join(this._config.get("path"), "symmetry-core");
|
34
|
+
const store = new corestore_1.default(corePath);
|
35
|
+
const core = new hyperdrive_1.default(store);
|
36
|
+
const swarm = new hyperswarm_1.default();
|
37
|
+
await core.ready();
|
38
|
+
const discovery = swarm.join(core.discoveryKey, { server: true });
|
39
|
+
await discovery.flushed();
|
40
|
+
swarm.on("connection", (peer) => {
|
41
|
+
logger_1.logger.info(`🔗 New Connection: ${peer.rawStream.remoteHost}`);
|
42
|
+
store.replicate(peer);
|
43
|
+
this.listeners(peer);
|
44
|
+
});
|
45
|
+
this._wsServer = new websocket_server_1.WsServer(this._config.get("webSocketPort"), this._peerRepository, swarm);
|
46
|
+
logger_1.logger.info(`🔑 Discovery key: ${(_a = core.discoveryKey) === null || _a === void 0 ? void 0 : _a.toString("hex")}`);
|
47
|
+
logger_1.logger.info(`🔑 Drive key: ${(_b = core.key) === null || _b === void 0 ? void 0 : _b.toString("hex")}`);
|
48
|
+
logger_1.logger.info(chalk_1.default.green(`\u2713 Websocket server started: ws://localhost:${this._config.get("webSocketPort")}`));
|
49
|
+
logger_1.logger.info(chalk_1.default.green(`\u2713 Symmetry server started, waiting for connections...`));
|
50
|
+
}
|
51
|
+
listeners(peer) {
|
52
|
+
peer.on("error", (err) => err);
|
53
|
+
peer.on("close", () => {
|
54
|
+
const peerKey = peer.publicKey.toString("hex");
|
55
|
+
this._peerRepository.updateLastSeen(peerKey);
|
56
|
+
logger_1.logger.info(`🔗 Connection Closed: Peer ${peerKey.slice(0, 6)}...${peerKey.slice(-6)}`);
|
57
|
+
});
|
58
|
+
peer.on("data", (message) => {
|
59
|
+
const data = (0, utils_1.safeParseJson)(message.toString());
|
60
|
+
if (!data)
|
61
|
+
return;
|
62
|
+
if (data.key) {
|
63
|
+
switch (data === null || data === void 0 ? void 0 : data.key) {
|
64
|
+
case constants_1.serverMessageKeys.join:
|
65
|
+
this.join(peer, data.data);
|
66
|
+
break;
|
67
|
+
case constants_1.serverMessageKeys.requestProvider:
|
68
|
+
this.handlePeerSession(peer, data.data);
|
69
|
+
break;
|
70
|
+
case constants_1.serverMessageKeys.verifySession:
|
71
|
+
this.handlePeerSessionValidation(peer, data.data);
|
72
|
+
break;
|
73
|
+
}
|
74
|
+
}
|
75
|
+
});
|
76
|
+
}
|
77
|
+
async join(peer, message) {
|
78
|
+
const peerKey = peer.publicKey.toString("hex");
|
79
|
+
try {
|
80
|
+
await this._peerRepository.upsert({
|
81
|
+
...message,
|
82
|
+
key: peerKey,
|
83
|
+
});
|
84
|
+
logger_1.logger.info(`👋 Peer provider joined ${peer.rawStream.remoteHost}`);
|
85
|
+
peer.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.joinAck, {
|
86
|
+
status: "success",
|
87
|
+
key: peerKey,
|
88
|
+
}));
|
89
|
+
}
|
90
|
+
catch (error) {
|
91
|
+
let errorMessage = "";
|
92
|
+
if (error instanceof Error)
|
93
|
+
errorMessage = error.message;
|
94
|
+
logger_1.logger.error(`🚨 ${errorMessage}`);
|
95
|
+
}
|
96
|
+
}
|
97
|
+
async handlePeerSession(peer, randomPeerRequest) {
|
98
|
+
try {
|
99
|
+
const providerPeer = await this._peerRepository.getPeer(randomPeerRequest);
|
100
|
+
const sessionToken = await this._sessionManager.createSession(providerPeer.discovery_key);
|
101
|
+
peer.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.providerDetails, {
|
102
|
+
providerId: providerPeer.key,
|
103
|
+
sessionToken,
|
104
|
+
}));
|
105
|
+
}
|
106
|
+
catch (error) {
|
107
|
+
let errorMessage = "";
|
108
|
+
if (error instanceof Error)
|
109
|
+
errorMessage = error.message;
|
110
|
+
logger_1.logger.error(`🚨 ${errorMessage}`);
|
111
|
+
}
|
112
|
+
}
|
113
|
+
async handlePeerSessionValidation(peer, message) {
|
114
|
+
if (!message.sessionToken)
|
115
|
+
return;
|
116
|
+
try {
|
117
|
+
const providerDiscoveryKey = await this._sessionManager.verifySession(message.sessionToken);
|
118
|
+
if (!providerDiscoveryKey)
|
119
|
+
return;
|
120
|
+
const providerPeer = await this._peerRepository.getByDiscoveryKey(providerDiscoveryKey);
|
121
|
+
if (!providerPeer)
|
122
|
+
return;
|
123
|
+
peer.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.sessionValid, {
|
124
|
+
discoveryKey: providerPeer.discovery_key,
|
125
|
+
}));
|
126
|
+
await this._sessionManager.extendSession(message.sessionToken);
|
127
|
+
}
|
128
|
+
catch (error) {
|
129
|
+
logger_1.logger.error(`Session verification error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
130
|
+
peer.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.sessionValid, {
|
131
|
+
valid: false,
|
132
|
+
error: "Error verifying session",
|
133
|
+
}));
|
134
|
+
}
|
135
|
+
}
|
136
|
+
async cleanupSessions() {
|
137
|
+
await this._sessionManager.cleanupExpiredSessions();
|
138
|
+
}
|
139
|
+
}
|
140
|
+
exports.SymmetryServer = SymmetryServer;
|
141
|
+
module.exports = {
|
142
|
+
SymmetryServer,
|
143
|
+
};
|
@@ -0,0 +1,68 @@
|
|
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.SessionManager = void 0;
|
7
|
+
const logger_1 = require("./logger");
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
9
|
+
class SessionManager {
|
10
|
+
constructor(sessionRepository, sessionDurationMinutes = 60) {
|
11
|
+
this.sessionRepository = sessionRepository;
|
12
|
+
this.sessionDuration = sessionDurationMinutes * 60 * 1000;
|
13
|
+
}
|
14
|
+
async createSession(providerId) {
|
15
|
+
const sessionId = crypto_1.default.randomUUID();
|
16
|
+
const now = new Date();
|
17
|
+
const session = {
|
18
|
+
id: sessionId,
|
19
|
+
providerId,
|
20
|
+
createdAt: now,
|
21
|
+
expiresAt: new Date(now.getTime() + this.sessionDuration),
|
22
|
+
};
|
23
|
+
await this.sessionRepository.create(session);
|
24
|
+
logger_1.logger.info(`🖇️ Session created for provider: ${providerId}`);
|
25
|
+
return sessionId;
|
26
|
+
}
|
27
|
+
async verifySession(sessionId) {
|
28
|
+
const session = await this.sessionRepository.get(sessionId);
|
29
|
+
if (!session) {
|
30
|
+
logger_1.logger.warning(`❌ Session not found: ${sessionId}`);
|
31
|
+
return null;
|
32
|
+
}
|
33
|
+
if (new Date() > session.expiresAt) {
|
34
|
+
logger_1.logger.warning(`🕛 Session expired: ${sessionId}`);
|
35
|
+
await this.sessionRepository.delete(sessionId);
|
36
|
+
return null;
|
37
|
+
}
|
38
|
+
return session.providerId;
|
39
|
+
}
|
40
|
+
async extendSession(sessionId) {
|
41
|
+
const session = await this.sessionRepository.get(sessionId);
|
42
|
+
if (!session) {
|
43
|
+
logger_1.logger.warning(`🚨 Cannot extend non-existent session: ${sessionId}`);
|
44
|
+
return false;
|
45
|
+
}
|
46
|
+
session.expiresAt = new Date(Date.now() + this.sessionDuration);
|
47
|
+
await this.sessionRepository.update(session);
|
48
|
+
logger_1.logger.info(`🎟️ Session extended: ${sessionId}`);
|
49
|
+
return true;
|
50
|
+
}
|
51
|
+
async deleteSession(sessionId) {
|
52
|
+
const result = await this.sessionRepository.delete(sessionId);
|
53
|
+
if (result) {
|
54
|
+
// cross bin emoji
|
55
|
+
logger_1.logger.info(`🗑 Session deleted: ${sessionId}`);
|
56
|
+
}
|
57
|
+
else {
|
58
|
+
logger_1.logger.warning(`🚨 Failed to delete session: ${sessionId}`);
|
59
|
+
}
|
60
|
+
return result;
|
61
|
+
}
|
62
|
+
async cleanupExpiredSessions() {
|
63
|
+
const deletedCount = await this.sessionRepository.deleteExpired();
|
64
|
+
logger_1.logger.info(`🕛 Cleaned up ${deletedCount} expired sessions`);
|
65
|
+
return deletedCount;
|
66
|
+
}
|
67
|
+
}
|
68
|
+
exports.SessionManager = SessionManager;
|
@@ -0,0 +1,127 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.SessionRepository = void 0;
|
4
|
+
const database_1 = require("./database");
|
5
|
+
class SessionRepository {
|
6
|
+
constructor() {
|
7
|
+
this.create = (session) => {
|
8
|
+
return new Promise((resolve, reject) => {
|
9
|
+
const sql = `INSERT INTO sessions (id, provider_id, created_at, expires_at) VALUES (?, ?, ?, ?)`;
|
10
|
+
this.db.run(sql, [
|
11
|
+
session.id,
|
12
|
+
session.providerId,
|
13
|
+
session.createdAt.toISOString(),
|
14
|
+
session.expiresAt.toISOString(),
|
15
|
+
], function (err) {
|
16
|
+
if (err) {
|
17
|
+
reject(err);
|
18
|
+
}
|
19
|
+
else {
|
20
|
+
resolve();
|
21
|
+
}
|
22
|
+
});
|
23
|
+
});
|
24
|
+
};
|
25
|
+
this.get = (id) => {
|
26
|
+
return new Promise((resolve, reject) => {
|
27
|
+
const sql = `
|
28
|
+
SELECT s.id, s.provider_id as providerId, s.created_at as createdAt, s.expires_at as expiresAt
|
29
|
+
FROM sessions s
|
30
|
+
WHERE s.id = ?
|
31
|
+
`;
|
32
|
+
this.db.get(sql, [id], (err, row) => {
|
33
|
+
if (err) {
|
34
|
+
reject(err);
|
35
|
+
}
|
36
|
+
else if (row) {
|
37
|
+
resolve({
|
38
|
+
id: row.id,
|
39
|
+
providerId: row.providerId,
|
40
|
+
createdAt: new Date(row.createdAt),
|
41
|
+
expiresAt: new Date(row.expiresAt),
|
42
|
+
});
|
43
|
+
}
|
44
|
+
else {
|
45
|
+
resolve(null);
|
46
|
+
}
|
47
|
+
});
|
48
|
+
});
|
49
|
+
};
|
50
|
+
this.update = (session) => {
|
51
|
+
return new Promise((resolve, reject) => {
|
52
|
+
const sql = `UPDATE sessions SET provider_id = ?, created_at = ?, expires_at = ? WHERE id = ?`;
|
53
|
+
this.db.run(sql, [
|
54
|
+
session.providerId,
|
55
|
+
session.createdAt.toISOString(),
|
56
|
+
session.expiresAt.toISOString(),
|
57
|
+
session.id,
|
58
|
+
], function (err) {
|
59
|
+
if (err) {
|
60
|
+
reject(err);
|
61
|
+
}
|
62
|
+
else if (this.changes === 0) {
|
63
|
+
reject(new Error("No session found with the provided id"));
|
64
|
+
}
|
65
|
+
else {
|
66
|
+
resolve();
|
67
|
+
}
|
68
|
+
});
|
69
|
+
});
|
70
|
+
};
|
71
|
+
this.delete = (id) => {
|
72
|
+
return new Promise((resolve, reject) => {
|
73
|
+
const sql = `DELETE FROM sessions WHERE id = ?`;
|
74
|
+
this.db.run(sql, [id], function (err) {
|
75
|
+
if (err) {
|
76
|
+
reject(err);
|
77
|
+
}
|
78
|
+
else {
|
79
|
+
resolve(this.changes > 0);
|
80
|
+
}
|
81
|
+
});
|
82
|
+
});
|
83
|
+
};
|
84
|
+
this.getAllActiveSessions = () => {
|
85
|
+
return new Promise((resolve, reject) => {
|
86
|
+
const sql = `
|
87
|
+
SELECT s.id, s.provider_id as providerId, s.created_at as createdAt, s.expires_at as expiresAt,
|
88
|
+
p.key as peer_key, p.discovery_key, p.model_name
|
89
|
+
FROM sessions s
|
90
|
+
LEFT JOIN peers p ON s.provider_id = p.id
|
91
|
+
WHERE s.expires_at > datetime('now')
|
92
|
+
`;
|
93
|
+
this.db.all(sql, [], (err, rows) => {
|
94
|
+
if (err) {
|
95
|
+
reject(err);
|
96
|
+
}
|
97
|
+
else {
|
98
|
+
resolve(rows.map((row) => ({
|
99
|
+
id: row.id,
|
100
|
+
providerId: row.providerId,
|
101
|
+
createdAt: new Date(row.createdAt),
|
102
|
+
expiresAt: new Date(row.expiresAt),
|
103
|
+
peer_key: row.peer_key,
|
104
|
+
discovery_key: row.discovery_key,
|
105
|
+
model_name: row.model_name,
|
106
|
+
})));
|
107
|
+
}
|
108
|
+
});
|
109
|
+
});
|
110
|
+
};
|
111
|
+
this.deleteExpired = () => {
|
112
|
+
return new Promise((resolve, reject) => {
|
113
|
+
const sql = `DELETE FROM sessions WHERE expires_at <= datetime('now')`;
|
114
|
+
this.db.run(sql, function (err) {
|
115
|
+
if (err) {
|
116
|
+
reject(err);
|
117
|
+
}
|
118
|
+
else {
|
119
|
+
resolve(this.changes);
|
120
|
+
}
|
121
|
+
});
|
122
|
+
});
|
123
|
+
};
|
124
|
+
this.db = database_1.database;
|
125
|
+
}
|
126
|
+
}
|
127
|
+
exports.SessionRepository = SessionRepository;
|
package/dist/symmetry.js
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
"use strict";
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
5
|
+
};
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
7
|
+
const commander_1 = require("commander");
|
8
|
+
const os_1 = __importDefault(require("os"));
|
9
|
+
const path_1 = __importDefault(require("path"));
|
10
|
+
const provider_1 = require("./provider");
|
11
|
+
const program = new commander_1.Command();
|
12
|
+
program
|
13
|
+
.version("1.0.0")
|
14
|
+
.description("symmetry cli")
|
15
|
+
.option("-c, --config <path>", "Path to config file", path_1.default.join(os_1.default.homedir(), ".config", "symmetry", "provider.yaml"))
|
16
|
+
.action(async () => {
|
17
|
+
const client = new provider_1.SymmetryProvider(program.opts().config);
|
18
|
+
await client.init();
|
19
|
+
});
|
20
|
+
program.parse(process.argv);
|
package/dist/types.js
ADDED
package/dist/utils.js
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.getChatDataFromProvider = void 0;
|
4
|
+
exports.safeParseJson = safeParseJson;
|
5
|
+
exports.createMessage = createMessage;
|
6
|
+
exports.isStreamWithDataPrefix = isStreamWithDataPrefix;
|
7
|
+
exports.safeParseStreamResponse = safeParseStreamResponse;
|
8
|
+
const constants_1 = require("./constants");
|
9
|
+
function safeParseJson(data) {
|
10
|
+
try {
|
11
|
+
return JSON.parse(data);
|
12
|
+
}
|
13
|
+
catch (e) {
|
14
|
+
return undefined;
|
15
|
+
}
|
16
|
+
}
|
17
|
+
function createMessage(key, data) {
|
18
|
+
return JSON.stringify({ key, data });
|
19
|
+
}
|
20
|
+
function isStreamWithDataPrefix(stringBuffer) {
|
21
|
+
return stringBuffer.startsWith('data:');
|
22
|
+
}
|
23
|
+
function safeParseStreamResponse(stringBuffer) {
|
24
|
+
try {
|
25
|
+
if (isStreamWithDataPrefix(stringBuffer)) {
|
26
|
+
return JSON.parse(stringBuffer.split('data:')[1]);
|
27
|
+
}
|
28
|
+
return JSON.parse(stringBuffer);
|
29
|
+
}
|
30
|
+
catch (e) {
|
31
|
+
return undefined;
|
32
|
+
}
|
33
|
+
}
|
34
|
+
const getChatDataFromProvider = (provider, data) => {
|
35
|
+
var _a, _b;
|
36
|
+
switch (provider) {
|
37
|
+
case constants_1.apiProviders.Ollama:
|
38
|
+
case constants_1.apiProviders.OpenWebUI:
|
39
|
+
return ((_a = data === null || data === void 0 ? void 0 : data.choices[0].delta) === null || _a === void 0 ? void 0 : _a.content)
|
40
|
+
? data === null || data === void 0 ? void 0 : data.choices[0].delta.content
|
41
|
+
: '';
|
42
|
+
case constants_1.apiProviders.LlamaCpp:
|
43
|
+
return data === null || data === void 0 ? void 0 : data.content;
|
44
|
+
case constants_1.apiProviders.LiteLLM:
|
45
|
+
default:
|
46
|
+
if ((data === null || data === void 0 ? void 0 : data.choices[0].delta.content) === 'undefined')
|
47
|
+
return '';
|
48
|
+
return ((_b = data === null || data === void 0 ? void 0 : data.choices[0].delta) === null || _b === void 0 ? void 0 : _b.content)
|
49
|
+
? data === null || data === void 0 ? void 0 : data.choices[0].delta.content
|
50
|
+
: '';
|
51
|
+
}
|
52
|
+
};
|
53
|
+
exports.getChatDataFromProvider = getChatDataFromProvider;
|