@xnetjs/plugins 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,1055 @@
1
+ // src/services/local-api.ts
2
+ import { timingSafeEqual } from "crypto";
3
+ import { createServer } from "http";
4
+ import { URL } from "url";
5
+ function constantTimeCompare(a, b) {
6
+ if (a.length !== b.length) return false;
7
+ const bufA = Buffer.from(a);
8
+ const bufB = Buffer.from(b);
9
+ return timingSafeEqual(bufA, bufB);
10
+ }
11
+ var EventBuffer = class {
12
+ events = [];
13
+ maxEvents;
14
+ constructor(maxEvents = 1e3) {
15
+ this.maxEvents = maxEvents;
16
+ }
17
+ push(event) {
18
+ this.events.push(event);
19
+ if (this.events.length > this.maxEvents) {
20
+ this.events.shift();
21
+ }
22
+ }
23
+ getSince(timestamp, schemaId) {
24
+ return this.events.filter((e) => {
25
+ if (e.timestamp <= timestamp) return false;
26
+ if (schemaId && e.node?.schemaId !== schemaId) return false;
27
+ return true;
28
+ });
29
+ }
30
+ };
31
+ var LocalAPIServer = class {
32
+ server = null;
33
+ config;
34
+ eventBuffer;
35
+ unsubscribe;
36
+ constructor(config) {
37
+ this.config = {
38
+ port: config.port ?? 31415,
39
+ host: config.host ?? "127.0.0.1",
40
+ token: config.token,
41
+ allowedOrigins: config.allowedOrigins ?? [],
42
+ store: config.store,
43
+ schemas: config.schemas
44
+ };
45
+ this.eventBuffer = new EventBuffer();
46
+ }
47
+ /**
48
+ * Start the API server
49
+ */
50
+ async start() {
51
+ if (this.server) {
52
+ throw new Error("Server already running");
53
+ }
54
+ this.unsubscribe = this.config.store.subscribe((event) => {
55
+ const type = this.getEventType(event.change.type);
56
+ if (type) {
57
+ this.eventBuffer.push({
58
+ type,
59
+ node: event.node,
60
+ timestamp: Date.now()
61
+ });
62
+ }
63
+ });
64
+ this.server = createServer((req, res) => {
65
+ this.handleRequest(req, res).catch((err) => {
66
+ console.error("[LocalAPI] Request error:", err);
67
+ this.sendError(res, 500, "Internal server error");
68
+ });
69
+ });
70
+ return new Promise((resolve, reject) => {
71
+ this.server.listen(this.config.port, this.config.host, () => {
72
+ console.log(`[LocalAPI] Server listening on http://${this.config.host}:${this.config.port}`);
73
+ resolve();
74
+ });
75
+ this.server.on("error", reject);
76
+ });
77
+ }
78
+ /**
79
+ * Stop the API server
80
+ */
81
+ async stop() {
82
+ if (this.unsubscribe) {
83
+ this.unsubscribe();
84
+ this.unsubscribe = void 0;
85
+ }
86
+ if (!this.server) return;
87
+ return new Promise((resolve) => {
88
+ this.server.close(() => {
89
+ console.log("[LocalAPI] Server stopped");
90
+ this.server = null;
91
+ resolve();
92
+ });
93
+ });
94
+ }
95
+ /**
96
+ * Get the server port
97
+ */
98
+ get port() {
99
+ return this.config.port;
100
+ }
101
+ /**
102
+ * Check if server is running
103
+ */
104
+ get isRunning() {
105
+ return this.server !== null;
106
+ }
107
+ // ─── Request Handling ────────────────────────────────────────────────────────
108
+ async handleRequest(req, res) {
109
+ const origin = req.headers.origin;
110
+ if (this.config.allowedOrigins.length > 0 && origin) {
111
+ const isAllowed = this.config.allowedOrigins.includes("*") || this.config.allowedOrigins.includes(origin);
112
+ if (isAllowed) {
113
+ res.setHeader("Access-Control-Allow-Origin", origin);
114
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
115
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
116
+ res.setHeader("Access-Control-Allow-Credentials", "true");
117
+ }
118
+ }
119
+ if (req.method === "OPTIONS") {
120
+ res.writeHead(204);
121
+ res.end();
122
+ return;
123
+ }
124
+ if (this.config.token) {
125
+ const auth = req.headers.authorization;
126
+ const expected = `Bearer ${this.config.token}`;
127
+ if (!auth || !constantTimeCompare(auth, expected)) {
128
+ this.sendError(res, 401, "Unauthorized");
129
+ return;
130
+ }
131
+ }
132
+ const url = new URL(req.url ?? "/", `http://${this.config.host}:${this.config.port}`);
133
+ const pathname = url.pathname;
134
+ const method = req.method ?? "GET";
135
+ try {
136
+ if (pathname === "/health" && method === "GET") {
137
+ return this.handleHealth(res);
138
+ }
139
+ if (pathname.startsWith("/api/v1/")) {
140
+ const path = pathname.slice("/api/v1".length);
141
+ if (path === "/nodes" && method === "GET") {
142
+ return await this.handleListNodes(url, res);
143
+ }
144
+ if (path === "/nodes" && method === "POST") {
145
+ return await this.handleCreateNode(req, res);
146
+ }
147
+ if (path.match(/^\/nodes\/[^/]+$/) && method === "GET") {
148
+ const id = path.slice("/nodes/".length);
149
+ return await this.handleGetNode(id, res);
150
+ }
151
+ if (path.match(/^\/nodes\/[^/]+$/) && method === "PATCH") {
152
+ const id = path.slice("/nodes/".length);
153
+ return await this.handleUpdateNode(id, req, res);
154
+ }
155
+ if (path.match(/^\/nodes\/[^/]+$/) && method === "DELETE") {
156
+ const id = path.slice("/nodes/".length);
157
+ return await this.handleDeleteNode(id, res);
158
+ }
159
+ if (path === "/query" && method === "POST") {
160
+ return await this.handleQuery(req, res);
161
+ }
162
+ if (path === "/events" && method === "GET") {
163
+ return this.handleGetEvents(url, res);
164
+ }
165
+ if (path === "/schemas" && method === "GET") {
166
+ return await this.handleListSchemas(res);
167
+ }
168
+ if (path.match(/^\/schemas\//) && method === "GET") {
169
+ const iri = decodeURIComponent(path.slice("/schemas/".length));
170
+ return await this.handleGetSchema(iri, res);
171
+ }
172
+ }
173
+ this.sendError(res, 404, "Not found");
174
+ } catch (err) {
175
+ console.error("[LocalAPI] Handler error:", err);
176
+ this.sendError(res, 500, err instanceof Error ? err.message : "Internal server error");
177
+ }
178
+ }
179
+ // ─── Health ──────────────────────────────────────────────────────────────────
180
+ handleHealth(res) {
181
+ this.sendJSON(res, 200, {
182
+ status: "ok",
183
+ version: "1.0.0",
184
+ timestamp: Date.now()
185
+ });
186
+ }
187
+ // ─── Nodes ───────────────────────────────────────────────────────────────────
188
+ async handleListNodes(url, res) {
189
+ const schemaId = url.searchParams.get("schema") ?? void 0;
190
+ const limit = parseInt(url.searchParams.get("limit") ?? "50", 10);
191
+ const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
192
+ const nodes = await this.config.store.list({ schemaId, limit, offset });
193
+ this.sendJSON(res, 200, { nodes, count: nodes.length, limit, offset });
194
+ }
195
+ async handleGetNode(id, res) {
196
+ const node = await this.config.store.get(id);
197
+ if (!node) {
198
+ this.sendError(res, 404, `Node not found: ${id}`);
199
+ return;
200
+ }
201
+ this.sendJSON(res, 200, node);
202
+ }
203
+ async handleCreateNode(req, res) {
204
+ const body = await this.parseBody(req);
205
+ if (!body.schema || typeof body.schema !== "string") {
206
+ this.sendError(res, 400, "Missing required field: schema");
207
+ return;
208
+ }
209
+ if (!body.properties || typeof body.properties !== "object") {
210
+ this.sendError(res, 400, "Missing required field: properties");
211
+ return;
212
+ }
213
+ const node = await this.config.store.create({
214
+ schemaId: body.schema,
215
+ properties: body.properties
216
+ });
217
+ this.sendJSON(res, 201, node);
218
+ }
219
+ async handleUpdateNode(id, req, res) {
220
+ const existing = await this.config.store.get(id);
221
+ if (!existing) {
222
+ this.sendError(res, 404, `Node not found: ${id}`);
223
+ return;
224
+ }
225
+ const body = await this.parseBody(req);
226
+ if (!body || typeof body !== "object") {
227
+ this.sendError(res, 400, "Request body must be an object");
228
+ return;
229
+ }
230
+ const node = await this.config.store.update(id, {
231
+ properties: body
232
+ });
233
+ this.sendJSON(res, 200, node);
234
+ }
235
+ async handleDeleteNode(id, res) {
236
+ const existing = await this.config.store.get(id);
237
+ if (!existing) {
238
+ this.sendError(res, 404, `Node not found: ${id}`);
239
+ return;
240
+ }
241
+ await this.config.store.delete(id);
242
+ this.sendJSON(res, 200, { success: true });
243
+ }
244
+ // ─── Query ───────────────────────────────────────────────────────────────────
245
+ async handleQuery(req, res) {
246
+ const body = await this.parseBody(req);
247
+ if (!body.schema || typeof body.schema !== "string") {
248
+ this.sendError(res, 400, "Missing required field: schema");
249
+ return;
250
+ }
251
+ const limit = typeof body.limit === "number" ? body.limit : 50;
252
+ const offset = typeof body.offset === "number" ? body.offset : 0;
253
+ const nodes = await this.config.store.list({
254
+ schemaId: body.schema,
255
+ limit,
256
+ offset
257
+ });
258
+ this.sendJSON(res, 200, { nodes, count: nodes.length });
259
+ }
260
+ // ─── Events ──────────────────────────────────────────────────────────────────
261
+ handleGetEvents(url, res) {
262
+ const since = parseInt(url.searchParams.get("since") ?? "0", 10);
263
+ const schemaId = url.searchParams.get("schema") ?? void 0;
264
+ const events = this.eventBuffer.getSince(since, schemaId);
265
+ this.sendJSON(res, 200, {
266
+ events,
267
+ timestamp: Date.now()
268
+ });
269
+ }
270
+ // ─── Schemas ─────────────────────────────────────────────────────────────────
271
+ async handleListSchemas(res) {
272
+ const iris = this.config.schemas.getAllIRIs();
273
+ const schemas = await Promise.all(
274
+ iris.map(async (iri) => {
275
+ const schema = await this.config.schemas.get(iri);
276
+ return schema ? {
277
+ iri,
278
+ name: schema.name,
279
+ properties: schema.properties
280
+ } : null;
281
+ })
282
+ );
283
+ this.sendJSON(res, 200, {
284
+ schemas: schemas.filter(Boolean)
285
+ });
286
+ }
287
+ async handleGetSchema(iri, res) {
288
+ const schema = await this.config.schemas.get(iri);
289
+ if (!schema) {
290
+ this.sendError(res, 404, `Schema not found: ${iri}`);
291
+ return;
292
+ }
293
+ this.sendJSON(res, 200, schema);
294
+ }
295
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
296
+ async parseBody(req) {
297
+ return new Promise((resolve, reject) => {
298
+ let body = "";
299
+ req.on("data", (chunk) => body += chunk);
300
+ req.on("end", () => {
301
+ try {
302
+ const parsed = body ? JSON.parse(body) : {};
303
+ resolve(parsed);
304
+ } catch {
305
+ reject(new Error("Invalid JSON body"));
306
+ }
307
+ });
308
+ req.on("error", reject);
309
+ });
310
+ }
311
+ sendJSON(res, status, data) {
312
+ res.writeHead(status, { "Content-Type": "application/json" });
313
+ res.end(JSON.stringify(data));
314
+ }
315
+ sendError(res, status, message) {
316
+ this.sendJSON(res, status, { error: message });
317
+ }
318
+ getEventType(changeType) {
319
+ if (changeType === "node-change") {
320
+ return "updated";
321
+ }
322
+ return null;
323
+ }
324
+ };
325
+ function createLocalAPI(config) {
326
+ return new LocalAPIServer(config);
327
+ }
328
+
329
+ // src/services/mcp-server.ts
330
+ var MCPServer = class {
331
+ config;
332
+ tools = /* @__PURE__ */ new Map();
333
+ running = false;
334
+ constructor(config) {
335
+ this.config = {
336
+ store: config.store,
337
+ schemas: config.schemas,
338
+ name: config.name ?? "xnet",
339
+ version: config.version ?? "1.0.0"
340
+ };
341
+ this.registerTools();
342
+ }
343
+ /**
344
+ * Get server info for MCP initialize response
345
+ */
346
+ getServerInfo() {
347
+ return {
348
+ name: this.config.name,
349
+ version: this.config.version
350
+ };
351
+ }
352
+ /**
353
+ * Get server capabilities
354
+ */
355
+ getCapabilities() {
356
+ return {
357
+ tools: {},
358
+ resources: {}
359
+ };
360
+ }
361
+ /**
362
+ * Get all registered tools
363
+ */
364
+ getTools() {
365
+ return Array.from(this.tools.values());
366
+ }
367
+ /**
368
+ * Get available resources
369
+ */
370
+ getResources() {
371
+ return [
372
+ {
373
+ uri: "xnet://nodes",
374
+ name: "All Nodes",
375
+ description: "List of all nodes in the local store",
376
+ mimeType: "application/json"
377
+ },
378
+ {
379
+ uri: "xnet://schemas",
380
+ name: "All Schemas",
381
+ description: "List of all available schemas",
382
+ mimeType: "application/json"
383
+ }
384
+ ];
385
+ }
386
+ /**
387
+ * Handle an MCP request
388
+ */
389
+ async handleRequest(request) {
390
+ const { id, method, params } = request;
391
+ try {
392
+ let result;
393
+ switch (method) {
394
+ case "initialize":
395
+ result = {
396
+ protocolVersion: "2024-11-05",
397
+ serverInfo: this.getServerInfo(),
398
+ capabilities: this.getCapabilities()
399
+ };
400
+ break;
401
+ case "tools/list":
402
+ result = { tools: this.getTools() };
403
+ break;
404
+ case "tools/call":
405
+ result = await this.handleToolCall(params);
406
+ break;
407
+ case "resources/list":
408
+ result = { resources: this.getResources() };
409
+ break;
410
+ case "resources/read":
411
+ result = await this.handleResourceRead(params);
412
+ break;
413
+ case "notifications/initialized":
414
+ result = {};
415
+ break;
416
+ default:
417
+ return {
418
+ jsonrpc: "2.0",
419
+ id,
420
+ error: { code: -32601, message: `Method not found: ${method}` }
421
+ };
422
+ }
423
+ return { jsonrpc: "2.0", id, result };
424
+ } catch (err) {
425
+ return {
426
+ jsonrpc: "2.0",
427
+ id,
428
+ error: {
429
+ code: -32e3,
430
+ message: err instanceof Error ? err.message : "Unknown error"
431
+ }
432
+ };
433
+ }
434
+ }
435
+ /**
436
+ * Start the MCP server in stdio mode.
437
+ * Reads JSON-RPC requests from stdin and writes responses to stdout.
438
+ */
439
+ async startStdio() {
440
+ if (this.running) {
441
+ throw new Error("Server already running");
442
+ }
443
+ this.running = true;
444
+ const readline = await import("readline");
445
+ const rl = readline.createInterface({
446
+ input: process.stdin,
447
+ output: process.stdout,
448
+ terminal: false
449
+ });
450
+ for await (const line of rl) {
451
+ if (!this.running) break;
452
+ try {
453
+ const request = JSON.parse(line);
454
+ const response = await this.handleRequest(request);
455
+ console.log(JSON.stringify(response));
456
+ } catch {
457
+ console.log(
458
+ JSON.stringify({
459
+ jsonrpc: "2.0",
460
+ id: null,
461
+ error: { code: -32700, message: "Parse error" }
462
+ })
463
+ );
464
+ }
465
+ }
466
+ }
467
+ /**
468
+ * Stop the stdio server
469
+ */
470
+ stop() {
471
+ this.running = false;
472
+ }
473
+ // ─── Tool Registration ─────────────────────────────────────────────────────
474
+ registerTools() {
475
+ this.tools.set("xnet_query", {
476
+ name: "xnet_query",
477
+ description: "Query nodes by schema and optional filter. Returns matching nodes with their properties.",
478
+ inputSchema: {
479
+ type: "object",
480
+ properties: {
481
+ schema: {
482
+ type: "string",
483
+ description: "Schema IRI to query (e.g., xnet://xnet.dev/Task)"
484
+ },
485
+ limit: {
486
+ type: "number",
487
+ description: "Maximum number of results to return (default: 20)"
488
+ },
489
+ offset: {
490
+ type: "number",
491
+ description: "Number of results to skip for pagination"
492
+ }
493
+ },
494
+ required: ["schema"]
495
+ }
496
+ });
497
+ this.tools.set("xnet_get", {
498
+ name: "xnet_get",
499
+ description: "Get a single node by its ID.",
500
+ inputSchema: {
501
+ type: "object",
502
+ properties: {
503
+ nodeId: {
504
+ type: "string",
505
+ description: "The unique ID of the node to retrieve"
506
+ }
507
+ },
508
+ required: ["nodeId"]
509
+ }
510
+ });
511
+ this.tools.set("xnet_create", {
512
+ name: "xnet_create",
513
+ description: "Create a new node with the given schema and properties.",
514
+ inputSchema: {
515
+ type: "object",
516
+ properties: {
517
+ schema: {
518
+ type: "string",
519
+ description: "Schema IRI for the new node"
520
+ },
521
+ properties: {
522
+ type: "object",
523
+ description: "Initial property values for the node"
524
+ }
525
+ },
526
+ required: ["schema", "properties"]
527
+ }
528
+ });
529
+ this.tools.set("xnet_update", {
530
+ name: "xnet_update",
531
+ description: "Update properties of an existing node.",
532
+ inputSchema: {
533
+ type: "object",
534
+ properties: {
535
+ nodeId: {
536
+ type: "string",
537
+ description: "ID of the node to update"
538
+ },
539
+ properties: {
540
+ type: "object",
541
+ description: "Properties to update (only specified properties will change)"
542
+ }
543
+ },
544
+ required: ["nodeId", "properties"]
545
+ }
546
+ });
547
+ this.tools.set("xnet_delete", {
548
+ name: "xnet_delete",
549
+ description: "Delete a node by its ID.",
550
+ inputSchema: {
551
+ type: "object",
552
+ properties: {
553
+ nodeId: {
554
+ type: "string",
555
+ description: "ID of the node to delete"
556
+ }
557
+ },
558
+ required: ["nodeId"]
559
+ }
560
+ });
561
+ this.tools.set("xnet_schemas", {
562
+ name: "xnet_schemas",
563
+ description: "List all available schemas with their properties. Useful for understanding what types of nodes can be created.",
564
+ inputSchema: {
565
+ type: "object",
566
+ properties: {}
567
+ }
568
+ });
569
+ }
570
+ // ─── Tool Execution ────────────────────────────────────────────────────────
571
+ async handleToolCall(params) {
572
+ const { name, arguments: args } = params;
573
+ const toolArgs = args ?? {};
574
+ let result;
575
+ switch (name) {
576
+ case "xnet_query": {
577
+ const schemaId = toolArgs.schema;
578
+ const limit = toolArgs.limit ?? 20;
579
+ const offset = toolArgs.offset ?? 0;
580
+ const nodes = await this.config.store.list({ schemaId, limit, offset });
581
+ result = { nodes, count: nodes.length };
582
+ break;
583
+ }
584
+ case "xnet_get": {
585
+ const nodeId = toolArgs.nodeId;
586
+ const node = await this.config.store.get(nodeId);
587
+ if (!node) {
588
+ throw new Error(`Node not found: ${nodeId}`);
589
+ }
590
+ result = node;
591
+ break;
592
+ }
593
+ case "xnet_create": {
594
+ const schema = toolArgs.schema;
595
+ const properties = toolArgs.properties;
596
+ const node = await this.config.store.create({
597
+ schemaId: schema,
598
+ properties
599
+ });
600
+ result = node;
601
+ break;
602
+ }
603
+ case "xnet_update": {
604
+ const nodeId = toolArgs.nodeId;
605
+ const properties = toolArgs.properties;
606
+ const existing = await this.config.store.get(nodeId);
607
+ if (!existing) {
608
+ throw new Error(`Node not found: ${nodeId}`);
609
+ }
610
+ const node = await this.config.store.update(nodeId, { properties });
611
+ result = node;
612
+ break;
613
+ }
614
+ case "xnet_delete": {
615
+ const nodeId = toolArgs.nodeId;
616
+ const existing = await this.config.store.get(nodeId);
617
+ if (!existing) {
618
+ throw new Error(`Node not found: ${nodeId}`);
619
+ }
620
+ await this.config.store.delete(nodeId);
621
+ result = { success: true, nodeId };
622
+ break;
623
+ }
624
+ case "xnet_schemas": {
625
+ const iris = this.config.schemas.getAllIRIs();
626
+ const schemas = await Promise.all(
627
+ iris.map(async (iri) => {
628
+ const schema = await this.config.schemas.get(iri);
629
+ return schema ? {
630
+ iri,
631
+ name: schema.name,
632
+ properties: schema.properties
633
+ } : null;
634
+ })
635
+ );
636
+ result = { schemas: schemas.filter(Boolean) };
637
+ break;
638
+ }
639
+ default:
640
+ throw new Error(`Unknown tool: ${name}`);
641
+ }
642
+ return {
643
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
644
+ };
645
+ }
646
+ // ─── Resource Handling ─────────────────────────────────────────────────────
647
+ async handleResourceRead(params) {
648
+ const { uri } = params;
649
+ let content;
650
+ if (uri === "xnet://nodes") {
651
+ const nodes = await this.config.store.list({ limit: 100 });
652
+ content = { nodes, count: nodes.length };
653
+ } else if (uri === "xnet://schemas") {
654
+ const iris = this.config.schemas.getAllIRIs();
655
+ const schemas = await Promise.all(
656
+ iris.map(async (iri) => {
657
+ const schema = await this.config.schemas.get(iri);
658
+ return schema ? { iri, name: schema.name } : null;
659
+ })
660
+ );
661
+ content = { schemas: schemas.filter(Boolean) };
662
+ } else {
663
+ throw new Error(`Resource not found: ${uri}`);
664
+ }
665
+ return {
666
+ contents: [
667
+ {
668
+ uri,
669
+ mimeType: "application/json",
670
+ text: JSON.stringify(content, null, 2)
671
+ }
672
+ ]
673
+ };
674
+ }
675
+ };
676
+ function createMCPServer(config) {
677
+ return new MCPServer(config);
678
+ }
679
+
680
+ // src/services/process-manager.ts
681
+ function isNodeEnvironment() {
682
+ return typeof process !== "undefined" && typeof process.versions !== "undefined" && typeof process.versions.node !== "undefined";
683
+ }
684
+ var ManagedProcess = class {
685
+ constructor(definition) {
686
+ this.definition = definition;
687
+ this.status = {
688
+ id: definition.id,
689
+ state: "stopped",
690
+ restartCount: 0
691
+ };
692
+ }
693
+ process = null;
694
+ status;
695
+ restartTimer = null;
696
+ healthCheckTimer = null;
697
+ listeners = { status: [], output: [] };
698
+ /**
699
+ * Start the service process
700
+ */
701
+ async start() {
702
+ if (this.status.state === "running" || this.status.state === "starting") {
703
+ return this.status;
704
+ }
705
+ if (!isNodeEnvironment()) {
706
+ throw new Error("ProcessManager can only run in Node.js environment");
707
+ }
708
+ const { spawn } = await import("child_process");
709
+ this.updateState("starting");
710
+ const { command, args = [], cwd, env, shell } = this.definition.process;
711
+ const { protocol } = this.definition.communication;
712
+ try {
713
+ this.process = spawn(command, args, {
714
+ cwd,
715
+ env: { ...process.env, ...env },
716
+ shell: shell ?? false,
717
+ stdio: protocol === "stdio" ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"]
718
+ });
719
+ this.status.pid = this.process.pid;
720
+ this.process.stdout?.on("data", (data) => {
721
+ this.emitOutput("stdout", data.toString());
722
+ });
723
+ this.process.stderr?.on("data", (data) => {
724
+ this.emitOutput("stderr", data.toString());
725
+ });
726
+ this.process.on("exit", (code, signal) => {
727
+ this.handleExit(code, signal);
728
+ });
729
+ this.process.on("error", (err) => {
730
+ this.status.lastError = err.message;
731
+ this.updateState("error");
732
+ });
733
+ await this.waitForHealthy();
734
+ this.updateState("running");
735
+ this.status.startedAt = Date.now();
736
+ this.startHealthCheck();
737
+ return this.getStatus();
738
+ } catch (err) {
739
+ this.status.lastError = err instanceof Error ? err.message : String(err);
740
+ this.updateState("error");
741
+ throw err;
742
+ }
743
+ }
744
+ /**
745
+ * Stop the service process
746
+ */
747
+ async stop() {
748
+ if (this.status.state === "stopped" || this.status.state === "stopping") {
749
+ return;
750
+ }
751
+ this.updateState("stopping");
752
+ this.clearTimers();
753
+ if (!this.process) {
754
+ this.updateState("stopped");
755
+ return;
756
+ }
757
+ const shutdownTimeout = this.definition.lifecycle.shutdownTimeoutMs ?? 5e3;
758
+ return new Promise((resolve) => {
759
+ const timeout = setTimeout(() => {
760
+ this.process?.kill("SIGKILL");
761
+ this.process = null;
762
+ this.updateState("stopped");
763
+ resolve();
764
+ }, shutdownTimeout);
765
+ this.process.on("exit", () => {
766
+ clearTimeout(timeout);
767
+ this.process = null;
768
+ this.updateState("stopped");
769
+ resolve();
770
+ });
771
+ this.process.kill("SIGTERM");
772
+ });
773
+ }
774
+ /**
775
+ * Get current status
776
+ */
777
+ getStatus() {
778
+ const status = { ...this.status };
779
+ if (status.state === "running" && status.startedAt) {
780
+ status.uptime = Date.now() - status.startedAt;
781
+ }
782
+ return status;
783
+ }
784
+ /**
785
+ * Subscribe to status changes
786
+ */
787
+ onStatus(callback) {
788
+ this.listeners.status.push(callback);
789
+ return () => {
790
+ const idx = this.listeners.status.indexOf(callback);
791
+ if (idx >= 0) this.listeners.status.splice(idx, 1);
792
+ };
793
+ }
794
+ /**
795
+ * Subscribe to output events
796
+ */
797
+ onOutput(callback) {
798
+ this.listeners.output.push(callback);
799
+ return () => {
800
+ const idx = this.listeners.output.indexOf(callback);
801
+ if (idx >= 0) this.listeners.output.splice(idx, 1);
802
+ };
803
+ }
804
+ // ─── Private Methods ─────────────────────────────────────────────────────────
805
+ updateState(state) {
806
+ const previousState = this.status.state;
807
+ this.status.state = state;
808
+ const event = {
809
+ serviceId: this.definition.id,
810
+ status: this.getStatus(),
811
+ previousState
812
+ };
813
+ for (const listener of this.listeners.status) {
814
+ try {
815
+ listener(event);
816
+ } catch (err) {
817
+ console.error("[ManagedProcess] Status listener error:", err);
818
+ }
819
+ }
820
+ }
821
+ emitOutput(stream, data) {
822
+ const event = {
823
+ serviceId: this.definition.id,
824
+ stream,
825
+ data,
826
+ timestamp: Date.now()
827
+ };
828
+ for (const listener of this.listeners.output) {
829
+ try {
830
+ listener(event);
831
+ } catch (err) {
832
+ console.error("[ManagedProcess] Output listener error:", err);
833
+ }
834
+ }
835
+ }
836
+ handleExit(code, signal) {
837
+ if (this.status.state === "stopping") {
838
+ return;
839
+ }
840
+ const { restart, maxRestarts = 5, restartDelayMs = 1e3 } = this.definition.lifecycle;
841
+ const shouldRestart = restart === "always" || restart === "on-failure" && code !== 0;
842
+ if (shouldRestart && this.status.restartCount < maxRestarts) {
843
+ this.status.restartCount++;
844
+ this.status.lastError = `Exited with code ${code}, signal ${signal}. Restarting...`;
845
+ this.updateState("starting");
846
+ this.restartTimer = setTimeout(() => {
847
+ this.start().catch((err) => {
848
+ console.error(`[ManagedProcess] Restart failed for ${this.definition.id}:`, err);
849
+ });
850
+ }, restartDelayMs);
851
+ } else {
852
+ this.status.lastError = `Exited with code ${code}, signal ${signal}`;
853
+ this.updateState("error");
854
+ }
855
+ }
856
+ async waitForHealthy() {
857
+ const { healthCheck, startTimeoutMs = 1e4 } = this.definition.lifecycle;
858
+ const start = Date.now();
859
+ while (Date.now() - start < startTimeoutMs) {
860
+ if (!healthCheck) {
861
+ await this.delay(500);
862
+ return;
863
+ }
864
+ const healthy = await this.checkHealth();
865
+ if (healthy) return;
866
+ await this.delay(500);
867
+ }
868
+ throw new Error(`Service '${this.definition.id}' health check timed out`);
869
+ }
870
+ async checkHealth() {
871
+ const { healthCheck } = this.definition.lifecycle;
872
+ if (!healthCheck) return true;
873
+ try {
874
+ switch (healthCheck.type) {
875
+ case "http": {
876
+ if (!healthCheck.url) return false;
877
+ const controller = new AbortController();
878
+ const timeout = setTimeout(() => controller.abort(), healthCheck.timeoutMs ?? 5e3);
879
+ try {
880
+ const res = await fetch(healthCheck.url, { signal: controller.signal });
881
+ return res.ok;
882
+ } finally {
883
+ clearTimeout(timeout);
884
+ }
885
+ }
886
+ case "tcp": {
887
+ const port = healthCheck.port ?? this.definition.communication.port;
888
+ if (!port) return false;
889
+ const { createConnection } = await import("net");
890
+ return new Promise((resolve) => {
891
+ const socket = createConnection(
892
+ { port, host: this.definition.communication.host ?? "127.0.0.1" },
893
+ () => {
894
+ socket.destroy();
895
+ resolve(true);
896
+ }
897
+ );
898
+ socket.on("error", () => resolve(false));
899
+ socket.setTimeout(healthCheck.timeoutMs ?? 5e3, () => {
900
+ socket.destroy();
901
+ resolve(false);
902
+ });
903
+ });
904
+ }
905
+ case "stdout": {
906
+ return this.process?.pid !== void 0;
907
+ }
908
+ default:
909
+ return false;
910
+ }
911
+ } catch {
912
+ return false;
913
+ }
914
+ }
915
+ startHealthCheck() {
916
+ const { healthCheck } = this.definition.lifecycle;
917
+ if (!healthCheck || !healthCheck.intervalMs) return;
918
+ this.healthCheckTimer = setInterval(async () => {
919
+ if (this.status.state !== "running") return;
920
+ const healthy = await this.checkHealth();
921
+ if (!healthy) {
922
+ this.status.lastError = "Health check failed";
923
+ }
924
+ }, healthCheck.intervalMs);
925
+ }
926
+ clearTimers() {
927
+ if (this.restartTimer) {
928
+ clearTimeout(this.restartTimer);
929
+ this.restartTimer = null;
930
+ }
931
+ if (this.healthCheckTimer) {
932
+ clearInterval(this.healthCheckTimer);
933
+ this.healthCheckTimer = null;
934
+ }
935
+ }
936
+ delay(ms) {
937
+ return new Promise((resolve) => setTimeout(resolve, ms));
938
+ }
939
+ };
940
+ var ProcessManager = class {
941
+ processes = /* @__PURE__ */ new Map();
942
+ listeners = {
943
+ "service:status": [],
944
+ "service:output": [],
945
+ "service:error": []
946
+ };
947
+ /**
948
+ * Start a service
949
+ */
950
+ async start(definition) {
951
+ if (this.processes.has(definition.id)) {
952
+ throw new Error(`Service '${definition.id}' is already registered`);
953
+ }
954
+ const managed = new ManagedProcess(definition);
955
+ this.processes.set(definition.id, managed);
956
+ managed.onStatus((event) => {
957
+ this.emit("service:status", event);
958
+ });
959
+ managed.onOutput((event) => {
960
+ this.emit("service:output", event);
961
+ });
962
+ try {
963
+ return await managed.start();
964
+ } catch (err) {
965
+ this.emit("service:error", {
966
+ serviceId: definition.id,
967
+ error: err instanceof Error ? err : new Error(String(err))
968
+ });
969
+ throw err;
970
+ }
971
+ }
972
+ /**
973
+ * Stop a service
974
+ */
975
+ async stop(serviceId) {
976
+ const managed = this.processes.get(serviceId);
977
+ if (!managed) return;
978
+ await managed.stop();
979
+ this.processes.delete(serviceId);
980
+ }
981
+ /**
982
+ * Restart a service
983
+ */
984
+ async restart(serviceId) {
985
+ const managed = this.processes.get(serviceId);
986
+ if (!managed) {
987
+ throw new Error(`Service '${serviceId}' not found`);
988
+ }
989
+ await managed.stop();
990
+ return managed.start();
991
+ }
992
+ /**
993
+ * Get service status
994
+ */
995
+ getStatus(serviceId) {
996
+ return this.processes.get(serviceId)?.getStatus();
997
+ }
998
+ /**
999
+ * Get all service statuses
1000
+ */
1001
+ getAllStatuses() {
1002
+ return [...this.processes.values()].map((p) => p.getStatus());
1003
+ }
1004
+ /**
1005
+ * Stop all services
1006
+ */
1007
+ async stopAll() {
1008
+ await Promise.all([...this.processes.keys()].map((id) => this.stop(id)));
1009
+ }
1010
+ /**
1011
+ * Subscribe to events
1012
+ */
1013
+ on(event, listener) {
1014
+ this.listeners[event].push(listener);
1015
+ }
1016
+ /**
1017
+ * Unsubscribe from events
1018
+ */
1019
+ off(event, listener) {
1020
+ const listeners = this.listeners[event];
1021
+ const idx = listeners.indexOf(listener);
1022
+ if (idx >= 0) listeners.splice(idx, 1);
1023
+ }
1024
+ emit(event, ...args) {
1025
+ const listeners = this.listeners[event];
1026
+ for (const listener of listeners) {
1027
+ try {
1028
+ const fn = listener;
1029
+ fn(...args);
1030
+ } catch (err) {
1031
+ console.error(`[ProcessManager] Event listener error for '${event}':`, err);
1032
+ }
1033
+ }
1034
+ }
1035
+ };
1036
+
1037
+ // src/services/client.ts
1038
+ var SERVICE_IPC_CHANNELS = {
1039
+ START: "xnet:service:start",
1040
+ STOP: "xnet:service:stop",
1041
+ RESTART: "xnet:service:restart",
1042
+ STATUS: "xnet:service:status",
1043
+ LIST_ALL: "xnet:service:list-all",
1044
+ CALL: "xnet:service:call",
1045
+ STATUS_UPDATE: "xnet:service:status-update",
1046
+ OUTPUT: "xnet:service:output"
1047
+ };
1048
+ export {
1049
+ LocalAPIServer,
1050
+ MCPServer,
1051
+ ProcessManager,
1052
+ SERVICE_IPC_CHANNELS,
1053
+ createLocalAPI,
1054
+ createMCPServer
1055
+ };