orbitx 0.0.1 → 0.0.2

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.
@@ -0,0 +1,153 @@
1
+ import * as net from "net";
2
+ import * as fs from "fs";
3
+ import MCPConnection from "./mcp-connection";
4
+
5
+ type Role = "server" | "client" | "pending";
6
+
7
+ export class MCPIPCConnection extends MCPConnection {
8
+ private readonly socketPath: string;
9
+ private socket: net.Socket | null = null;
10
+ private server: net.Server | null = null;
11
+ private role: Role = "pending";
12
+ private writeBuffer: Buffer[] = [];
13
+ private readBuffer = "";
14
+
15
+ constructor(socketPath: string) {
16
+ super(true);
17
+ this.socketPath = socketPath;
18
+
19
+ this.on("write", (data: unknown) => {
20
+ this._sendJSON(data);
21
+ });
22
+
23
+ setTimeout(() => {
24
+ this._tryConnect();
25
+ }, Math.random() * 200); // Pervent Race Condtion
26
+ }
27
+
28
+ close(): void {
29
+ this.socket?.destroy();
30
+ this.server?.close();
31
+ this._cleanSocketFile();
32
+ }
33
+
34
+ private _tryConnect(): void {
35
+ const sock = net.createConnection({ path: this.socketPath });
36
+
37
+ sock.once("connect", () => {
38
+ this.role = "client";
39
+ this.emit("role", "client");
40
+ this._attachSocket(sock);
41
+ });
42
+
43
+ sock.once("error", (err: NodeJS.ErrnoException) => {
44
+ if (err.code === "ENOENT" || err.code === "ECONNREFUSED") {
45
+ sock.destroy();
46
+ this._becomeServer();
47
+ } else {
48
+ this.emit("error", err);
49
+ }
50
+ });
51
+ }
52
+
53
+ private _becomeServer(): void {
54
+ this._cleanSocketFile();
55
+
56
+ const srv = net.createServer((peer) => {
57
+ this.role = "server";
58
+ this.emit("role", "server");
59
+
60
+ srv.close();
61
+
62
+ this._attachSocket(peer);
63
+ });
64
+
65
+ srv.on("error", (err) => this.emit("error", err));
66
+
67
+ srv.listen({ path: this.socketPath }, () => {
68
+ this.server = srv;
69
+ this.emit("listening", this.socketPath);
70
+ });
71
+ }
72
+
73
+ private _attachSocket(sock: net.Socket): void {
74
+ this.socket = sock;
75
+
76
+ for (const chunk of this.writeBuffer) {
77
+ sock.write(chunk);
78
+ }
79
+ this.writeBuffer = [];
80
+
81
+ sock.setEncoding("utf8");
82
+
83
+ sock.on("data", (chunk: string) => {
84
+ this._handleIncoming(chunk);
85
+ });
86
+
87
+ sock.on("end", () => {
88
+ this.emit("disconnected");
89
+ this._maybeReconnect();
90
+ });
91
+
92
+ sock.on("error", (err: NodeJS.ErrnoException) => {
93
+ this.emit("error", err);
94
+ });
95
+
96
+ this.emit("connected", this.role);
97
+ }
98
+
99
+ private _sendJSON(data: unknown): void {
100
+ let line: string;
101
+ try {
102
+ line = JSON.stringify(data) + "\n";
103
+ } catch (err) {
104
+ this.emit("error", new Error(`MCPIPCConnection: cannot serialise data – ${err}`));
105
+ return;
106
+ }
107
+
108
+ const chunk = Buffer.from(line, "utf8");
109
+
110
+ if (this.socket && !this.socket.destroyed) {
111
+ this.socket.write(chunk);
112
+ } else {
113
+ this.writeBuffer.push(chunk);
114
+ }
115
+ }
116
+
117
+ private _handleIncoming(chunk: string): void {
118
+ this.readBuffer += chunk;
119
+
120
+ const lines = this.readBuffer.split("\n");
121
+
122
+ this.readBuffer = lines.pop() ?? "";
123
+
124
+ for (const line of lines) {
125
+ const trimmed = line.trim();
126
+ if (!trimmed) continue;
127
+
128
+ try {
129
+ const parsed = JSON.parse(trimmed);
130
+ this.emit("read", parsed);
131
+ } catch {
132
+ this.emit("error", new Error(`MCPIPCConnection: malformed JSON – ${trimmed}`));
133
+ }
134
+ }
135
+ }
136
+
137
+ private _cleanSocketFile(): void {
138
+ try {
139
+ fs.unlinkSync(this.socketPath);
140
+ } catch { }
141
+ }
142
+
143
+ private _maybeReconnect(): void {
144
+ if (this.role === "client") {
145
+ setTimeout(() => {
146
+ this.role = "pending";
147
+ this._tryConnect();
148
+ }, 500);
149
+ }
150
+ }
151
+ }
152
+
153
+ export default MCPIPCConnection;
@@ -0,0 +1,49 @@
1
+ import type MCPTool from "./mcp";
2
+ import type MCPConnection from "./mcp-connection";
3
+
4
+ export class MCPServer {
5
+ #connection;
6
+ #tools: MCPTool[] = [];
7
+ constructor(connection: MCPConnection) {
8
+ this.#connection = connection;
9
+ this.#connection.on("read", (data: any) => {
10
+ const topic = data.topic;
11
+ const pid = data.pid;
12
+ if (topic == "getTools") {
13
+ this.#connection.emit("write", {
14
+ pid,
15
+ topic: "getToolsCallback",
16
+ tools: this.#tools.map(tool => ({
17
+ name: tool.getOptions().name,
18
+ description: tool.getOptions().description,
19
+ inputs: tool.getOptions().inputs
20
+ }))
21
+ });
22
+ }
23
+ if (topic == "toolCall") {
24
+ const tool = this.#tools.find(t => t.getOptions().name == data.tool);
25
+ if (tool) {
26
+ tool.getOptions().execute(data.envID, data.inputs).then((response) => {
27
+ this.#connection.emit("write", {
28
+ pid,
29
+ topic: "toolCallCallback",
30
+ output: response
31
+ });
32
+ }).catch(error => {
33
+ this.#connection.emit("write", {
34
+ pid,
35
+ topic: "toolCallCallback",
36
+ output: { error: error.message }
37
+ });
38
+ });
39
+ }
40
+ }
41
+ });
42
+ }
43
+
44
+ registerTool(tool: MCPTool) {
45
+ this.#tools.push(tool);
46
+ }
47
+ }
48
+
49
+ export default MCPServer;
@@ -0,0 +1,23 @@
1
+ export class MCPTool {
2
+ #options;
3
+ constructor(options: {
4
+ name: string;
5
+ description: string;
6
+ inputs: {
7
+ name: string;
8
+ type: "number" | "string" | "boolean" | "object" | "array";
9
+ description: string;
10
+ required?: boolean;
11
+ default?: any;
12
+ }[];
13
+ execute: (envID: string, inputs: Record<string, any>) => Promise<any>;
14
+ }) {
15
+ this.#options = options;
16
+ }
17
+
18
+ getOptions() {
19
+ return this.#options;
20
+ }
21
+ }
22
+
23
+ export default MCPTool;
@@ -0,0 +1,70 @@
1
+ import { Ollama } from "ollama";
2
+ import AIProvider, { type Message, type ChatResponse, type StreamCallback } from "./ai-provider";
3
+
4
+ export class OllamaProvider extends AIProvider {
5
+ private client: Ollama;
6
+ private model: string;
7
+
8
+ constructor(model: string, host: string = "http://localhost:11434") {
9
+ super();
10
+ this.client = new Ollama({ host });
11
+ this.model = model;
12
+ }
13
+
14
+ async chat(
15
+ messages: Message[],
16
+ streamCallback?: StreamCallback
17
+ ): Promise<ChatResponse> {
18
+ const formattedMessages = messages.map(msg => ({
19
+ role: msg.role,
20
+ content: msg.content || ""
21
+ }));
22
+
23
+ if (streamCallback) {
24
+ const stream = await this.client.chat({
25
+ model: this.model,
26
+ messages: formattedMessages as any,
27
+ stream: true
28
+ });
29
+
30
+ let fullContent = "";
31
+ let promptEvalCount = 0;
32
+ let evalCount = 0;
33
+
34
+ for await (const chunk of stream) {
35
+ const content = chunk.message?.content || "";
36
+ if (content) {
37
+ fullContent += content;
38
+ streamCallback({ role: "assistant", content, done: false });
39
+ }
40
+
41
+ if (chunk.prompt_eval_count !== undefined) {
42
+ promptEvalCount = chunk.prompt_eval_count;
43
+ }
44
+ if (chunk.eval_count !== undefined) {
45
+ evalCount = chunk.eval_count;
46
+ }
47
+ }
48
+
49
+ streamCallback({ role: "assistant", content: "", done: true });
50
+ return {
51
+ content: fullContent,
52
+ inputTokens: promptEvalCount,
53
+ outputTokens: evalCount
54
+ };
55
+ } else {
56
+ const response = await this.client.chat({
57
+ model: this.model,
58
+ messages: formattedMessages as any
59
+ });
60
+
61
+ return {
62
+ content: response.message?.content || "",
63
+ inputTokens: response.prompt_eval_count || 0,
64
+ outputTokens: response.eval_count || 0
65
+ };
66
+ }
67
+ }
68
+ }
69
+
70
+ export default OllamaProvider;
@@ -0,0 +1,16 @@
1
+ export class Skill {
2
+ #skill;
3
+ constructor(skill: {
4
+ name: string;
5
+ description: string;
6
+ instructions: string;
7
+ }) {
8
+ this.#skill = skill;
9
+ }
10
+
11
+ getSkill() {
12
+ return this.#skill;
13
+ }
14
+ }
15
+
16
+ export default Skill;
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export { AIProvider } from "./core/ai-provider";
2
+ export { BaseAgent } from "./core/base-agent";
3
+ export { DeepSeekProvider } from "./core/deepseek-provider";
4
+ export { MCPClient } from "./core/mcp-client";
5
+ export { MCPConnection } from "./core/mcp-connection";
6
+ export { MCPIPCConnection } from "./core/mcp-ipc-connection";
7
+ export { MCPServer } from "./core/mcp-server";
8
+ export { MCPTool } from "./core/mcp";
9
+ export { OllamaProvider } from "./core/ollama-provider";
10
+ export { Skill } from "./core/skill";
11
+
12
+ export { ResearcherAgent } from "./templates/researcher";
13
+ export { SimpleAgent } from "./templates/simple";
14
+
15
+ export { ReadFileTool, WriteFileTool } from "./tools/fs";
16
+ export { GetCurrentTimeTool } from "./tools/utils";
17
+ export { FetchTool, ReadWebPageTool, WebSearchTool } from "./tools/www";
@@ -0,0 +1,73 @@
1
+ import type { Message } from "../core/ai-provider";
2
+ import type AIProvider from "../core/ai-provider";
3
+ import BaseAgent from "../core/base-agent";
4
+ import type MCPTool from "../core/mcp";
5
+ import MCPClient from "../core/mcp-client";
6
+ import MCPConnection from "../core/mcp-connection";
7
+ import MCPServer from "../core/mcp-server";
8
+ import type Skill from "../core/skill";
9
+ import { ReadWebPageTool, WebSearchTool } from "../tools/www";
10
+
11
+ export class ResearcherAgent extends BaseAgent {
12
+ constructor({
13
+ aiProvider,
14
+ instruction = `you are research agent you have some rules you don't know about any true otherwise first search about it
15
+ your each topic search iteration contain two step
16
+ 1. use web-search tool to get some urls from search engine
17
+ 2. read 4 url with best topic near to out search query in this step you execute 4 read-web-page tool call at once
18
+ if result is resolve needs we stop otherwise do above operation more
19
+ YOU MUST DO two step before decide to response don't do web-search alone!! at less do search and read page operation 3 times to to get as much information as possible`,
20
+ tools = [
21
+ WebSearchTool,
22
+ ReadWebPageTool
23
+ ],
24
+ skills = [],
25
+ maxMemorizeToken = 30000,
26
+ initData = {
27
+ memory: "",
28
+ messagesFull: [],
29
+ fullInputMissTokens: 0,
30
+ fullInputHitTokens: 0,
31
+ fullOutputTokens: 0,
32
+ messagesCompact: [],
33
+ currentInputMissTokens: 0,
34
+ currentInputHitTokens: 0,
35
+ currentOutputTokens: 0,
36
+ }
37
+ }: {
38
+ instruction?: string;
39
+ tools?: MCPTool[];
40
+ aiProvider: AIProvider;
41
+ skills?: Skill[];
42
+ maxMemorizeToken?: number;
43
+ initData?: {
44
+ memory: string;
45
+ messagesFull: Message[];
46
+ fullInputMissTokens: number;
47
+ fullInputHitTokens: number;
48
+ fullOutputTokens: number;
49
+ messagesCompact: Message[];
50
+ currentInputMissTokens: number;
51
+ currentInputHitTokens: number;
52
+ currentOutputTokens: number;
53
+ }
54
+ }) {
55
+ const conn = new MCPConnection();
56
+ const mcpServer = new MCPServer(conn);
57
+ tools.forEach(tool => {
58
+ mcpServer.registerTool(tool);
59
+ });
60
+ const mcpClient = new MCPClient("DEFAULT_ENV", conn);
61
+ super({
62
+ aiProvider,
63
+ instruction,
64
+ mcpClient,
65
+ allowedTools: tools,
66
+ skills,
67
+ maxMemorizeToken,
68
+ initData,
69
+ });
70
+ }
71
+ }
72
+
73
+ export default ResearcherAgent;
@@ -0,0 +1,64 @@
1
+ import type { Message } from "../core/ai-provider";
2
+ import type AIProvider from "../core/ai-provider";
3
+ import BaseAgent from "../core/base-agent";
4
+ import type MCPTool from "../core/mcp";
5
+ import MCPClient from "../core/mcp-client";
6
+ import MCPConnection from "../core/mcp-connection";
7
+ import MCPServer from "../core/mcp-server";
8
+ import type Skill from "../core/skill";
9
+
10
+ export class SimpleAgent extends BaseAgent {
11
+ constructor({
12
+ aiProvider,
13
+ instruction = "",
14
+ tools = [],
15
+ skills = [],
16
+ maxMemorizeToken = 16000,
17
+ initData = {
18
+ memory: "",
19
+ messagesFull: [],
20
+ fullInputMissTokens: 0,
21
+ fullInputHitTokens: 0,
22
+ fullOutputTokens: 0,
23
+ messagesCompact: [],
24
+ currentInputMissTokens: 0,
25
+ currentInputHitTokens: 0,
26
+ currentOutputTokens: 0,
27
+ }
28
+ }: {
29
+ instruction: string;
30
+ tools: MCPTool[];
31
+ aiProvider: AIProvider;
32
+ skills?: Skill[];
33
+ maxMemorizeToken?: number;
34
+ initData?: {
35
+ memory: string;
36
+ messagesFull: Message[];
37
+ fullInputMissTokens: number;
38
+ fullInputHitTokens: number;
39
+ fullOutputTokens: number;
40
+ messagesCompact: Message[];
41
+ currentInputMissTokens: number;
42
+ currentInputHitTokens: number;
43
+ currentOutputTokens: number;
44
+ }
45
+ }) {
46
+ const conn = new MCPConnection();
47
+ const mcpServer = new MCPServer(conn);
48
+ tools.forEach(tool => {
49
+ mcpServer.registerTool(tool);
50
+ });
51
+ const mcpClient = new MCPClient("DEFAULT_ENV", conn);
52
+ super({
53
+ aiProvider,
54
+ instruction,
55
+ mcpClient,
56
+ allowedTools: tools,
57
+ skills,
58
+ maxMemorizeToken,
59
+ initData,
60
+ });
61
+ }
62
+ }
63
+
64
+ export default SimpleAgent;
@@ -0,0 +1,73 @@
1
+ import { describe, test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import os from "os";
4
+ import MCPIPCConnection from "../core/mcp-ipc-connection";
5
+ import MCPServer from "../core/mcp-server";
6
+ import MCPClient from "../core/mcp-client";
7
+ import { GetCurrentTimeTool } from "../tools/utils";
8
+ import { FetchTool, WebSearchTool } from "../tools/www";
9
+
10
+ describe("MCP Client and Server", async () => {
11
+ const path1 = os.platform() == "win32" ? `\\\\.\\pipe\\test${Math.random() * 1000}` : `/tmp/mcp_test${Math.random() * 1000}.sock`;
12
+ const path2 = os.platform() == "win32" ? `\\\\.\\pipe\\test${Math.random() * 1000}` : `/tmp/mcp_test${Math.random() * 1000}.sock`;
13
+
14
+ const ipcConnection11 = new MCPIPCConnection(path1);
15
+ const ipcConnection21 = new MCPIPCConnection(path2);
16
+
17
+ const ipcConnection12 = new MCPIPCConnection(path1);
18
+ const ipcConnection22 = new MCPIPCConnection(path2);
19
+
20
+ let mcpServer1: MCPServer;
21
+ let mcpServer2: MCPServer;
22
+ let mcpClient: MCPClient;
23
+
24
+ await test("should connect MCP Client to Servers and get empty tools", async () => {
25
+ mcpServer1 = new MCPServer(ipcConnection11);
26
+ mcpServer2 = new MCPServer(ipcConnection21);
27
+ mcpClient = new MCPClient("DEFAULT_ENV", [ipcConnection12, ipcConnection22]);
28
+ const result = await mcpClient.getTools();
29
+ assert.equal(result.length, 0);
30
+ });
31
+
32
+ await test("should register tool on client side", async () => {
33
+ mcpClient.registerTool(GetCurrentTimeTool);
34
+ const result = await mcpClient.getTools();
35
+
36
+ assert.equal(result.length, 1);
37
+ });
38
+
39
+ await test("should register tool on server1 side", async () => {
40
+ mcpServer1.registerTool(FetchTool);
41
+ const result = await mcpClient.getTools();
42
+
43
+ assert.equal(result.length, 2);
44
+ });
45
+
46
+ await test("should register tool on server2 side", async () => {
47
+ mcpServer2.registerTool(WebSearchTool);
48
+ const result = await mcpClient.getTools();
49
+
50
+ assert.equal(result.length, 3);
51
+ });
52
+
53
+ await test("should call tool on client side", async () => {
54
+ const result = await mcpClient.callTool(GetCurrentTimeTool.getOptions().name, {});
55
+
56
+ assert.ok(typeof result == "string");
57
+ });
58
+
59
+ await test("should call tool on server1 side", async () => {
60
+ const result = await mcpClient.callTool(FetchTool.getOptions().name, { url: "https://google.com" });
61
+
62
+ assert.ok(typeof result == "string");
63
+ });
64
+
65
+ await test("should call tool on server2 side", async () => {
66
+ const result = await mcpClient.callTool(WebSearchTool.getOptions().name, { query: "test" });
67
+
68
+ assert.ok(typeof result == "string");
69
+ });
70
+
71
+
72
+ // Need Close Connections
73
+ });
@@ -0,0 +1,2 @@
1
+ export { ReadFileTool } from "./read-file";
2
+ export { WriteFileTool } from "./write-file";
@@ -0,0 +1,54 @@
1
+ import MCPTool from "../../core/mcp";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+
5
+ export const ReadFileTool = new MCPTool({
6
+ name: "read-file",
7
+ description: "Reads text content from a file at the specified path.",
8
+ inputs: [{
9
+ name: "filePath",
10
+ type: "string",
11
+ description: `Absolute or relative path to the file (e.g., "/home/user/doc.txt" or "./file.txt")`,
12
+ required: true,
13
+ }],
14
+ execute: async (_: string, inputs: Record<string, any>): Promise<any> => {
15
+ try {
16
+ const { filePath } = inputs;
17
+
18
+ if (!filePath || typeof filePath !== "string") {
19
+ throw new Error("filePath must be a non-empty string");
20
+ }
21
+
22
+ const normalizedPath = path.normalize(filePath);
23
+
24
+ const data = await fs.readFile(normalizedPath, "utf-8");
25
+
26
+ return {
27
+ message: `File read successfully: ${normalizedPath}`,
28
+ path: normalizedPath,
29
+ content: data,
30
+ size: data.length
31
+ };
32
+
33
+ } catch (error) {
34
+ let errorMessage = error instanceof Error ? error.message : String(error);
35
+
36
+ if (error instanceof Error && "code" in error) {
37
+ const err = error as NodeJS.ErrnoException;
38
+ if (err.code === "ENOENT") {
39
+ errorMessage = `File not found: ${inputs.filePath}`;
40
+ } else if (err.code === "EACCES") {
41
+ errorMessage = `Permission denied: ${inputs.filePath}`;
42
+ } else if (err.code === "EISDIR") {
43
+ errorMessage = `Path is a directory, not a file: ${inputs.filePath}`;
44
+ }
45
+ }
46
+
47
+ return {
48
+ error: errorMessage
49
+ };
50
+ }
51
+ }
52
+ });
53
+
54
+ export default ReadFileTool;
@@ -0,0 +1,73 @@
1
+ import MCPTool from "../../core/mcp";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+
5
+ export const WriteFileTool = new MCPTool({
6
+ name: "write-file",
7
+ description: "Writes text content to a file at the specified path. Creates new file or overwrites existing.",
8
+ inputs: [{
9
+ name: "filePath",
10
+ type: "string",
11
+ description: `Absolute or relative path to the file (e.g., "/home/user/doc.txt" or "./file.txt")`,
12
+ required: true,
13
+ },
14
+ {
15
+ name: "data",
16
+ type: "string",
17
+ description: "Text content to write (supports multi-line)",
18
+ required: true,
19
+ }],
20
+ execute: async (_: string, inputs: Record<string, any>): Promise<any> => {
21
+ try {
22
+ const { filePath, data } = inputs;
23
+
24
+ if (!filePath || typeof filePath !== "string") {
25
+ throw new Error("filePath must be a non-empty string");
26
+ }
27
+
28
+ if (typeof data !== "string") {
29
+ throw new Error("data must be a string");
30
+ }
31
+
32
+ const normalizedPath = path.normalize(filePath);
33
+
34
+ const dir = path.dirname(normalizedPath);
35
+ await fs.mkdir(dir, { recursive: true });
36
+
37
+ await fs.writeFile(normalizedPath, data, "utf-8");
38
+
39
+ return {
40
+ message: `File written: ${normalizedPath}`,
41
+ path: normalizedPath,
42
+ size: data.length
43
+ };
44
+
45
+ } catch (error) {
46
+ let errorMessage = error instanceof Error ? error.message : String(error);
47
+
48
+ if (error instanceof Error && "code" in error) {
49
+ const err = error as NodeJS.ErrnoException;
50
+
51
+ if (err.code === "EACCES") {
52
+ errorMessage = `Permission denied: Cannot write to ${inputs.filePath}`;
53
+ } else if (err.code === "ENOENT") {
54
+ errorMessage = `Directory does not exist or cannot be created: ${path.dirname(inputs.filePath)}`;
55
+ } else if (err.code === "EISDIR") {
56
+ errorMessage = `Path is a directory, cannot write file: ${inputs.filePath}`;
57
+ } else if (err.code === "ENOSPC") {
58
+ errorMessage = `No space left on device: Cannot write to ${inputs.filePath}`;
59
+ } else if (err.code === "EROFS") {
60
+ errorMessage = `Read-only file system: Cannot write to ${inputs.filePath}`;
61
+ } else if (err.code === "ENAMETOOLONG") {
62
+ errorMessage = `File path is too long: ${inputs.filePath}`;
63
+ }
64
+ }
65
+
66
+ return {
67
+ error: errorMessage
68
+ };
69
+ }
70
+ }
71
+ });
72
+
73
+ export default WriteFileTool;