symmetry-cli 1.0.7 → 1.0.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -28,4 +28,3 @@ jobs:
28
28
  cache: 'npm'
29
29
  - run: npm ci
30
30
  - run: npm run build --if-present
31
- - run: npm test
package/dist/symmetry.js CHANGED
@@ -5,16 +5,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const commander_1 = require("commander");
8
+ const symmetry_core_1 = require("symmetry-core");
8
9
  const os_1 = __importDefault(require("os"));
9
10
  const path_1 = __importDefault(require("path"));
10
- const provider_1 = require("./provider");
11
11
  const program = new commander_1.Command();
12
12
  program
13
13
  .version("1.0.0")
14
14
  .description("symmetry cli")
15
15
  .option("-c, --config <path>", "Path to config file", path_1.default.join(os_1.default.homedir(), ".config", "symmetry", "provider.yaml"))
16
16
  .action(async () => {
17
- const client = new provider_1.SymmetryProvider(program.opts().config);
17
+ const client = new symmetry_core_1.SymmetryProvider(program.opts().config);
18
18
  await client.init();
19
19
  });
20
20
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symmetry-cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.11",
4
4
  "description": "",
5
5
  "main": "dist/symmetry.js",
6
6
  "bin": "dist/symmetry.js",
@@ -16,14 +16,8 @@
16
16
  "lint": "eslint . --ext .ts,.js --fix"
17
17
  },
18
18
  "dependencies": {
19
- "chalk": "^4.1.2",
20
19
  "commander": "^12.1.0",
21
- "hypercore-crypto": "^3.4.2",
22
- "hyperdht": "^6.15.4",
23
- "hyperswarm": "^4.8.0",
24
- "js-yaml": "^4.1.0",
25
- "sqlite3": "^5.1.7",
26
- "ws": "^8.18.0"
20
+ "symmetry-core": "^1.0.5"
27
21
  },
28
22
  "devDependencies": {
29
23
  "@types/jest": "^29.5.12",
@@ -38,6 +32,5 @@
38
32
  "ts-jest": "^29.2.2",
39
33
  "ts-node": "^10.9.2",
40
34
  "typescript": "^5.5.4"
41
- },
42
- "types": "types/twinnymind.d.ts"
35
+ }
43
36
  }
package/readme.md CHANGED
@@ -34,6 +34,22 @@ To start Symmetry, run:
34
34
  symmetry-cli
35
35
  ```
36
36
 
37
+ You will then be joined with the symmetry server and ready for connections!
38
+
39
+ ```bash
40
+ ℹ️ INFO: 🔗 Initializing client using config file: /home/twinnydotdev/.config/symmetry/provider.yaml
41
+ ℹ️ INFO: 📁 Symmetry client initialized.
42
+ ℹ️ INFO: 🔑 Discovery key: xxx
43
+ ℹ️ INFO: 🔑 Server key: 4b4a9cc325d134dee6679e9407420023531fd7e96c563f6c5d00fd5549b77435
44
+ ℹ️ INFO: 🔗 Joining server, please wait.
45
+ ℹ️ INFO: 🔗 Connected to server.
46
+ ℹ️ INFO: ✅ Verification successful.
47
+ ℹ️ INFO: 👋 Saying hello to your provider...
48
+ ℹ️ INFO: 🚀 Sending test request to http://localhost:11434/v1/chat/completions
49
+ ℹ️ INFO: 📡 Got response, checking stream...
50
+ ℹ️ INFO: ✅ Test inference call successful!
51
+ ```
52
+
37
53
  By default, Symmetry looks for its configuration file at `~/.config/symmetry/provider.yaml`. To use a different configuration file, use:
38
54
 
39
55
  ```bash
package/src/symmetry.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
+ import { SymmetryProvider } from 'symmetry-core'
3
4
  import os from "os";
4
5
  import path from "path";
5
6
 
6
- import { SymmetryProvider } from "./provider";
7
-
8
7
  const program = new Command();
9
8
 
10
9
  program
@@ -1,62 +0,0 @@
1
- import { SymmetryProvider } from "../src/provider";
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: SymmetryProvider;
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: false,
49
- serverKey: "test-server-key",
50
- systemMessage: "test-system-message",
51
- };
52
-
53
- beforeEach(() => {
54
- jest.clearAllMocks();
55
- (yaml.load as jest.Mock).mockReturnValue(mockConfig);
56
- writer = new SymmetryProvider("mock-config.yaml");
57
- });
58
-
59
- test("init method sets up the writer correctly", async () => {
60
- await writer.init();
61
- });
62
- });
package/dist/client.js DELETED
@@ -1,230 +0,0 @@
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;
@@ -1,58 +0,0 @@
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 DELETED
@@ -1,44 +0,0 @@
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;
package/dist/constants.js DELETED
@@ -1,31 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.apiProviders = exports.serverMessageKeys = exports.PROVIDER_HELLO_TIMEOUT = exports.NORMALIZE_REGEX = void 0;
4
- exports.NORMALIZE_REGEX = /\s*\r?\n|\r/g;
5
- exports.PROVIDER_HELLO_TIMEOUT = 15000;
6
- exports.serverMessageKeys = {
7
- challenge: "challenge",
8
- conectionSize: "conectionSize",
9
- heartbeat: "heartbeat",
10
- inference: "inference",
11
- inferenceEnded: "inferenceEnded",
12
- join: "join",
13
- joinAck: "joinAck",
14
- leave: "leave",
15
- newConversation: "newConversation",
16
- ping: "ping",
17
- pong: "pong",
18
- providerDetails: "providerDetails",
19
- reportCompletion: "reportCompletion",
20
- requestProvider: "requestProvider",
21
- sessionValid: "sessionValid",
22
- verifySession: "verifySession",
23
- };
24
- exports.apiProviders = {
25
- LiteLLM: 'litellm',
26
- LlamaCpp: 'llamacpp',
27
- LMStudio: 'lmstudio',
28
- Ollama: 'ollama',
29
- Oobabooga: 'oobabooga',
30
- OpenWebUI: 'openwebui',
31
- };
package/dist/database.js DELETED
@@ -1,9 +0,0 @@
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 DELETED
@@ -1,45 +0,0 @@
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();