@nebulaos/client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1765 @@
1
+ // src/config/client-config.ts
2
+ import { z } from "zod";
3
+ var serverConfigSchema = z.object({
4
+ url: z.string().min(1),
5
+ apiKey: z.string().min(1),
6
+ batchSize: z.number().int().positive().optional(),
7
+ flushInterval: z.number().int().positive().optional()
8
+ });
9
+ var otelTracingConfigSchema = z.object({
10
+ /** OTLP HTTP endpoint. Defaults to http://localhost:4318/v1/traces */
11
+ otlpEndpoint: z.string().optional()
12
+ });
13
+ var clientConfigSchema = z.object({
14
+ /**
15
+ * @deprecated clientId is now automatically extracted from the API key.
16
+ * This field is optional and only used for local identification/logging.
17
+ */
18
+ clientId: z.string().min(1).optional(),
19
+ /**
20
+ * List of agents to register. Can be either:
21
+ * - Agent instances (backwards compatible, but may cause memory leakage between executions)
22
+ * - Factory functions that return agent instances (recommended for proper isolation)
23
+ */
24
+ agents: z.array(z.custom()).optional(),
25
+ tools: z.array(z.custom()).optional(),
26
+ workflows: z.array(z.custom()).optional(),
27
+ server: serverConfigSchema.optional(),
28
+ /**
29
+ * OpenTelemetry tracing configuration.
30
+ * When set, the client will set up an OTel TracerProvider and export
31
+ * spans directly to the configured OTLP endpoint.
32
+ */
33
+ tracing: otelTracingConfigSchema.optional(),
34
+ /**
35
+ * Client-side log level (same semantics as @nebulaos/core ConsoleLogger).
36
+ * When set to anything other than "none", the client will emit debug logs for
37
+ * WS/HTTP communication (without full payloads).
38
+ */
39
+ logLevel: z.custom().optional(),
40
+ // Backwards compatibility (deprecated): prefer `logLevel`.
41
+ debug: z.object({
42
+ enabled: z.boolean().optional(),
43
+ level: z.custom().optional()
44
+ }).optional()
45
+ });
46
+
47
+ // src/errors/client-errors.ts
48
+ var ClientError = class extends Error {
49
+ constructor(message, code, statusCode = 500, details) {
50
+ super(message);
51
+ this.code = code;
52
+ this.statusCode = statusCode;
53
+ this.details = details;
54
+ this.name = "ClientError";
55
+ }
56
+ };
57
+ var RegistryError = class extends ClientError {
58
+ constructor(message, details) {
59
+ super(message, "REGISTRY_ERROR", 400, details);
60
+ this.name = "RegistryError";
61
+ }
62
+ };
63
+ var NotFoundError = class extends ClientError {
64
+ constructor(entity, id) {
65
+ super(`${entity} '${id}' not found`, "NOT_FOUND", 404);
66
+ this.name = "NotFoundError";
67
+ }
68
+ };
69
+ var AuthenticationError = class extends ClientError {
70
+ constructor(message = "Authentication failed: Invalid API key or missing credentials", details) {
71
+ super(message, "AUTHENTICATION_ERROR", 401, details);
72
+ this.name = "AuthenticationError";
73
+ }
74
+ };
75
+ var ConnectionError = class extends ClientError {
76
+ constructor(message, details) {
77
+ super(message, "CONNECTION_ERROR", 0, details);
78
+ this.name = "ConnectionError";
79
+ }
80
+ };
81
+
82
+ // src/registry/registry.ts
83
+ var Registry = class {
84
+ agentsById = /* @__PURE__ */ new Map();
85
+ agentsByName = /* @__PURE__ */ new Map();
86
+ workflows = /* @__PURE__ */ new Map();
87
+ toolsById = /* @__PURE__ */ new Map();
88
+ httpClient;
89
+ /**
90
+ * Sets the HTTP client to be injected into agents for tool execution.
91
+ */
92
+ setHttpClient(client) {
93
+ this.httpClient = client;
94
+ }
95
+ /**
96
+ * Registers an agent factory or instance.
97
+ *
98
+ * If an instance is passed directly (backwards compatibility), it will be wrapped
99
+ * in a factory that always returns the same instance. For proper isolation between
100
+ * executions, prefer passing a factory function.
101
+ *
102
+ * @param agentOrFactory - Agent instance or factory function
103
+ */
104
+ registerAgent(agentOrFactory) {
105
+ let factory;
106
+ let sampleAgent;
107
+ if (typeof agentOrFactory === "function") {
108
+ factory = agentOrFactory;
109
+ sampleAgent = factory();
110
+ } else {
111
+ const instance = agentOrFactory;
112
+ factory = () => instance;
113
+ sampleAgent = instance;
114
+ }
115
+ const id = "id" in sampleAgent && typeof sampleAgent.id === "string" && sampleAgent.id ? sampleAgent.id : sampleAgent.name;
116
+ if (this.agentsById.has(id)) {
117
+ throw new RegistryError(`Agent '${id}' already registered`);
118
+ }
119
+ if (this.agentsByName.has(sampleAgent.name)) {
120
+ throw new RegistryError(`Agent name '${sampleAgent.name}' already registered`);
121
+ }
122
+ const entry = {
123
+ factory,
124
+ id,
125
+ name: sampleAgent.name
126
+ };
127
+ this.agentsById.set(id, entry);
128
+ this.agentsByName.set(sampleAgent.name, entry);
129
+ }
130
+ registerWorkflow(workflow) {
131
+ const workflowId = workflow.id;
132
+ if (!workflowId) {
133
+ throw new RegistryError("Workflow id is required to register workflow");
134
+ }
135
+ if (this.workflows.has(workflowId)) {
136
+ throw new RegistryError(`Workflow '${workflowId}' already registered`);
137
+ }
138
+ this.workflows.set(workflowId, workflow);
139
+ }
140
+ registerTool(tool) {
141
+ const id = tool?.id;
142
+ if (!id || typeof id !== "string") {
143
+ throw new RegistryError("Tool id is required to register tool");
144
+ }
145
+ if (this.toolsById.has(id)) {
146
+ throw new RegistryError(`Tool '${id}' already registered`);
147
+ }
148
+ this.toolsById.set(id, tool);
149
+ }
150
+ /**
151
+ * Gets a new agent instance by id or name.
152
+ *
153
+ * Each call creates a fresh instance via the registered factory,
154
+ * ensuring no memory/context leakage between executions.
155
+ *
156
+ * @param idOrName - Agent id or name
157
+ * @returns A new agent instance
158
+ */
159
+ getAgent(idOrName) {
160
+ const entry = this.agentsById.get(idOrName) ?? this.agentsByName.get(idOrName);
161
+ if (!entry) {
162
+ throw new NotFoundError("agent", idOrName);
163
+ }
164
+ const agent = entry.factory();
165
+ if (this.httpClient && typeof agent.setHttpClient === "function") {
166
+ agent.setHttpClient(this.httpClient);
167
+ }
168
+ return agent;
169
+ }
170
+ getWorkflow(id) {
171
+ const workflow = this.workflows.get(id);
172
+ if (!workflow) {
173
+ throw new NotFoundError("workflow", id);
174
+ }
175
+ return workflow;
176
+ }
177
+ getTool(id) {
178
+ const tool = this.toolsById.get(id);
179
+ if (!tool) {
180
+ throw new NotFoundError("tool", id);
181
+ }
182
+ return tool;
183
+ }
184
+ /**
185
+ * Lists all registered agents by creating a sample instance of each.
186
+ *
187
+ * Used for Cloud registration where we need agent metadata (name, id, tools, etc).
188
+ * Creates one instance per agent to extract metadata.
189
+ *
190
+ * @returns Array of agent instances (one per registered agent)
191
+ */
192
+ listAgents() {
193
+ return Array.from(this.agentsById.values()).map((entry) => entry.factory());
194
+ }
195
+ listTools() {
196
+ return Array.from(this.toolsById.values());
197
+ }
198
+ listWorkflows() {
199
+ return Array.from(this.workflows.values());
200
+ }
201
+ };
202
+
203
+ // src/connection/server-connection.ts
204
+ import { io } from "socket.io-client";
205
+ import { v4 as uuid } from "uuid";
206
+ import { ExecutionContext, Tracing } from "@nebulaos/core";
207
+ import { zodToJsonSchema } from "zod-to-json-schema";
208
+
209
+ // src/logger/time.ts
210
+ function nowMs() {
211
+ const anyGlobal = globalThis;
212
+ if (anyGlobal?.performance?.now) return anyGlobal.performance.now();
213
+ return Date.now();
214
+ }
215
+ function durationMs(startMs) {
216
+ return Math.max(0, nowMs() - startMs);
217
+ }
218
+
219
+ // src/logger/summaries.ts
220
+ function summarizeValue(value) {
221
+ if (value === null) return "null";
222
+ if (value === void 0) return "undefined";
223
+ if (typeof value === "string") return `string(len=${value.length})`;
224
+ if (typeof value === "number") return "number";
225
+ if (typeof value === "boolean") return "boolean";
226
+ if (Array.isArray(value)) return `array(len=${value.length})`;
227
+ if (typeof value === "object") return `object(keys=${Object.keys(value).length})`;
228
+ return typeof value;
229
+ }
230
+ function summarizeDomainEvent(event) {
231
+ return {
232
+ type: event.type,
233
+ executionId: event.executionId,
234
+ rootExecutionId: event.rootExecutionId,
235
+ parentExecutionId: event.parentExecutionId,
236
+ correlationId: event.correlationId,
237
+ traceId: event.trace?.traceId,
238
+ spanId: event.trace?.spanId,
239
+ parentSpanId: event.trace?.parentSpanId
240
+ };
241
+ }
242
+ function summarizeTelemetryEvent(event) {
243
+ return {
244
+ type: event.type,
245
+ executionId: event.executionId,
246
+ correlationId: event.correlationId,
247
+ traceId: event.trace?.traceId,
248
+ spanId: event.trace?.spanId,
249
+ parentSpanId: event.trace?.parentSpanId,
250
+ spanKind: event.span?.kind,
251
+ spanName: event.span?.name
252
+ };
253
+ }
254
+ function summarizeDomainBatch(events) {
255
+ const counts = {};
256
+ for (const e of events) {
257
+ counts[e.type] = (counts[e.type] ?? 0) + 1;
258
+ }
259
+ const topTypes = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 6).map(([type, count]) => `${type}\xD7${count}`);
260
+ return {
261
+ count: events.length,
262
+ topTypes: topTypes.join(", ")
263
+ };
264
+ }
265
+
266
+ // src/connection/server-connection.ts
267
+ var ServerConnection = class {
268
+ constructor(config, logger) {
269
+ this.config = config;
270
+ this.clientId = config.clientId;
271
+ this.instanceId = config.instanceId ?? uuid();
272
+ this.logger = logger;
273
+ this.socket = this.createSocket();
274
+ }
275
+ socket;
276
+ reconnectAttempts = 0;
277
+ registry;
278
+ agents = [];
279
+ tools = [];
280
+ workflows = [];
281
+ snapshotTimer;
282
+ connectionAttempted = false;
283
+ lastConnectError = null;
284
+ hasAuthenticated = false;
285
+ logger;
286
+ commandStarts = /* @__PURE__ */ new Map();
287
+ clientId;
288
+ instanceId;
289
+ /**
290
+ * Sets the registry for executing agents and workflows.
291
+ */
292
+ setRegistry(registry) {
293
+ this.registry = registry;
294
+ }
295
+ /**
296
+ * Stores agents and workflows for registration.
297
+ */
298
+ registerResources(agents, tools, workflows) {
299
+ this.agents = agents;
300
+ this.tools = tools;
301
+ this.workflows = workflows;
302
+ }
303
+ collectAgentsFromWorkflows() {
304
+ const out = [];
305
+ const seen = /* @__PURE__ */ new Set();
306
+ for (const workflow of this.workflows) {
307
+ const workflowWithNodes = workflow;
308
+ if (typeof workflowWithNodes.__internalNodes !== "function") continue;
309
+ const nodes = workflowWithNodes.__internalNodes();
310
+ if (!Array.isArray(nodes)) continue;
311
+ for (const node of nodes) {
312
+ if (node.type !== "agent") continue;
313
+ const agentNode = node;
314
+ const agent = agentNode.agent;
315
+ if (!agent) continue;
316
+ const id = agent.id ?? agent.name;
317
+ if (!id) continue;
318
+ if (seen.has(id)) continue;
319
+ seen.add(id);
320
+ out.push(agent);
321
+ }
322
+ }
323
+ return out;
324
+ }
325
+ collectToolsFromAgents(agents) {
326
+ const out = [];
327
+ const seen = /* @__PURE__ */ new Set();
328
+ for (const agent of agents) {
329
+ const agentWithConfig = agent;
330
+ const config = agentWithConfig.config;
331
+ if (!config || typeof config !== "object") continue;
332
+ const tools = Array.isArray(config.tools) ? config.tools : [];
333
+ for (const tool of tools) {
334
+ if (!tool || typeof tool.id !== "string") continue;
335
+ if (seen.has(tool.id)) continue;
336
+ seen.add(tool.id);
337
+ out.push(tool);
338
+ }
339
+ }
340
+ return out;
341
+ }
342
+ /**
343
+ * Creates and configures the Socket.IO client.
344
+ */
345
+ createSocket() {
346
+ const socket = io(`${this.config.url}/runtime`, {
347
+ auth: { token: this.config.apiKey },
348
+ reconnection: true,
349
+ reconnectionDelay: 1e3,
350
+ reconnectionDelayMax: 5e3,
351
+ transports: ["websocket"]
352
+ });
353
+ this.setupEventHandlers(socket);
354
+ return socket;
355
+ }
356
+ /**
357
+ * Sets up Socket.IO event handlers
358
+ */
359
+ setupEventHandlers(socket) {
360
+ socket.on("connect", () => {
361
+ if (this.logger) {
362
+ this.logger.info("WS connected", { url: this.config.url, transport: "websocket" });
363
+ } else {
364
+ console.log("\u2705 Connected to NebulaOS Cloud");
365
+ }
366
+ this.reconnectAttempts = 0;
367
+ this.lastConnectError = null;
368
+ this.hasAuthenticated = false;
369
+ });
370
+ socket.on("disconnect", (reason) => {
371
+ if (!this.hasAuthenticated && reason === "io server disconnect") {
372
+ if (this.logger) {
373
+ this.logger.error("WS rejected by server (likely auth failure)", { reason });
374
+ } else {
375
+ console.error(
376
+ `\u{1F510} Connection rejected by server (likely authentication failure). Please check your NEBULAOS_API_KEY environment variable. (Reason: ${reason})`
377
+ );
378
+ }
379
+ } else {
380
+ if (this.logger) {
381
+ this.logger.warn("WS disconnected", { reason });
382
+ } else {
383
+ console.warn(`\u26A0\uFE0F Disconnected: ${reason}`);
384
+ }
385
+ }
386
+ });
387
+ socket.on("connect_error", (error) => {
388
+ this.reconnectAttempts++;
389
+ this.lastConnectError = error;
390
+ const rawMessage = error instanceof Error ? error.message : String(error);
391
+ const rawStack = error instanceof Error ? error.stack : void 0;
392
+ const errorWithData = error;
393
+ const serverData = errorWithData.data;
394
+ const serverMessage = typeof serverData?.message === "string" ? serverData.message : typeof serverData?.error === "string" ? serverData.error : void 0;
395
+ const lower = rawMessage.toLowerCase();
396
+ const looksLikeAuth = lower.includes("authorization") || lower.includes("token") || lower.includes("unauthorized") || lower.includes("authentication") || lower.includes("api key") || lower.includes("invalid");
397
+ const meta = {
398
+ url: this.config.url,
399
+ attempt: this.reconnectAttempts,
400
+ serverMessage,
401
+ serverData,
402
+ // Preserve raw stack for debugging (dev use)
403
+ stack: rawStack,
404
+ hint: looksLikeAuth ? "Looks like auth failure. Check NEBULAOS_API_KEY." : "See error details above (network/DNS/URL/CORS)."
405
+ };
406
+ if (this.logger) {
407
+ this.logger.error(`WS connect_error: ${rawMessage}`, meta);
408
+ } else {
409
+ console.error(`\u274C connect_error (attempt ${this.reconnectAttempts}): ${rawMessage}`, meta);
410
+ }
411
+ });
412
+ socket.on("client:online", () => {
413
+ this.hasAuthenticated = true;
414
+ if (this.logger) {
415
+ this.logger.info("WS authenticated (client:online)");
416
+ }
417
+ void this.registerFull();
418
+ });
419
+ socket.on(
420
+ "command:execute:agent",
421
+ (payload) => void this.handleAgentCommand(payload)
422
+ );
423
+ socket.on(
424
+ "command:execute:workflow",
425
+ (payload) => void this.handleWorkflowCommand(payload)
426
+ );
427
+ socket.on(
428
+ "command:execute:tool",
429
+ (payload) => void this.handleToolCommand(payload)
430
+ );
431
+ }
432
+ /**
433
+ * Handles agent execution commands from the server
434
+ */
435
+ async handleAgentCommand(payload) {
436
+ const startedAt = nowMs();
437
+ this.commandStarts.set(payload.commandId, startedAt);
438
+ if (this.logger) {
439
+ this.logger.info("Command received", {
440
+ type: payload.type,
441
+ target: payload.targetName,
442
+ commandId: payload.commandId,
443
+ executionId: payload.executionId,
444
+ input: summarizeValue(payload.input)
445
+ });
446
+ } else {
447
+ console.log(`\u{1F4E5} Received agent execution command: ${payload.targetName}`);
448
+ }
449
+ if (!this.registry) {
450
+ this.sendError(payload.commandId, {
451
+ code: "REGISTRY_NOT_SET",
452
+ message: "Registry not configured"
453
+ });
454
+ return;
455
+ }
456
+ try {
457
+ const agent = this.registry.getAgent(payload.targetName);
458
+ let agentInput;
459
+ if (payload.input) {
460
+ if (typeof payload.input === "string") {
461
+ agentInput = payload.input;
462
+ } else if (Array.isArray(payload.input)) {
463
+ agentInput = payload.input;
464
+ } else {
465
+ agentInput = JSON.stringify(payload.input);
466
+ }
467
+ }
468
+ const options = payload.options && typeof payload.options === "object" ? { ...payload.options, executionId: payload.executionId } : { executionId: payload.executionId };
469
+ const result = await ExecutionContext.run(
470
+ {
471
+ executionId: payload.executionId,
472
+ rootExecutionId: payload.executionId,
473
+ parentExecutionId: void 0
474
+ },
475
+ async () => agent.execute(agentInput, options)
476
+ );
477
+ this.sendResponse(payload.commandId, payload.executionId, result);
478
+ if (this.logger) {
479
+ this.logger.info("Command handled", {
480
+ commandId: payload.commandId,
481
+ executionId: payload.executionId,
482
+ durationMs: Number(durationMs(startedAt).toFixed(2))
483
+ });
484
+ }
485
+ } catch (error) {
486
+ this.sendError(payload.commandId, {
487
+ code: "AGENT_EXECUTION_ERROR",
488
+ message: error instanceof Error ? error.message : "Unknown error",
489
+ details: error
490
+ });
491
+ }
492
+ }
493
+ /**
494
+ * Handles workflow execution commands from the server
495
+ */
496
+ async handleWorkflowCommand(payload) {
497
+ const startedAt = nowMs();
498
+ this.commandStarts.set(payload.commandId, startedAt);
499
+ if (this.logger) {
500
+ this.logger.info("Command received", {
501
+ type: payload.type,
502
+ target: payload.targetName,
503
+ commandId: payload.commandId,
504
+ executionId: payload.executionId,
505
+ input: summarizeValue(payload.input)
506
+ });
507
+ } else {
508
+ console.log(`\u{1F4E5} Received workflow execution command: ${payload.targetName}`);
509
+ }
510
+ if (!this.registry) {
511
+ this.sendError(payload.commandId, {
512
+ code: "REGISTRY_NOT_SET",
513
+ message: "Registry not configured"
514
+ });
515
+ return;
516
+ }
517
+ try {
518
+ const workflow = this.registry.getWorkflow(payload.targetName);
519
+ const result = await ExecutionContext.run(
520
+ {
521
+ executionId: payload.executionId,
522
+ rootExecutionId: payload.executionId,
523
+ parentExecutionId: void 0
524
+ },
525
+ async () => workflow.run(
526
+ payload.input,
527
+ { executionId: payload.executionId }
528
+ )
529
+ );
530
+ this.sendResponse(payload.commandId, payload.executionId, result);
531
+ if (this.logger) {
532
+ this.logger.info("Command handled", {
533
+ commandId: payload.commandId,
534
+ executionId: payload.executionId,
535
+ durationMs: Number(durationMs(startedAt).toFixed(2))
536
+ });
537
+ }
538
+ } catch (error) {
539
+ this.sendError(payload.commandId, {
540
+ code: "WORKFLOW_EXECUTION_ERROR",
541
+ message: error instanceof Error ? error.message : "Unknown error",
542
+ details: error
543
+ });
544
+ }
545
+ }
546
+ /**
547
+ * Handles tool execution commands from the server.
548
+ */
549
+ async handleToolCommand(payload) {
550
+ const startedAt = nowMs();
551
+ this.commandStarts.set(payload.commandId, startedAt);
552
+ if (this.logger) {
553
+ this.logger.info("Command received", {
554
+ type: payload.type,
555
+ target: payload.targetName,
556
+ commandId: payload.commandId,
557
+ executionId: payload.executionId,
558
+ input: summarizeValue(payload.input)
559
+ });
560
+ } else {
561
+ console.log(`\u{1F4E5} Received tool execution command: ${payload.targetName}`);
562
+ }
563
+ if (!this.registry) {
564
+ this.sendError(payload.commandId, {
565
+ code: "REGISTRY_NOT_SET",
566
+ message: "Registry not configured"
567
+ });
568
+ return;
569
+ }
570
+ try {
571
+ const tool = this.registry.getTool(payload.targetName);
572
+ const result = await ExecutionContext.run(
573
+ {
574
+ executionId: payload.executionId,
575
+ rootExecutionId: payload.executionId,
576
+ parentExecutionId: void 0
577
+ },
578
+ async () => Tracing.withSpan(
579
+ {
580
+ kind: "tool",
581
+ name: `tool:${tool.id}`,
582
+ executionId: payload.executionId,
583
+ data: {
584
+ toolId: tool.id
585
+ }
586
+ },
587
+ async () => {
588
+ return tool.execute({}, payload.input);
589
+ }
590
+ )
591
+ );
592
+ this.sendResponse(payload.commandId, payload.executionId, result);
593
+ if (this.logger) {
594
+ this.logger.info("Command handled", {
595
+ commandId: payload.commandId,
596
+ executionId: payload.executionId,
597
+ durationMs: Number(durationMs(startedAt).toFixed(2))
598
+ });
599
+ }
600
+ } catch (error) {
601
+ this.sendError(payload.commandId, {
602
+ code: "TOOL_EXECUTION_ERROR",
603
+ message: error instanceof Error ? error.message : "Unknown error",
604
+ details: error
605
+ });
606
+ }
607
+ }
608
+ /**
609
+ * Executes an agent and waits for completion
610
+ */
611
+ // NOTE:
612
+ // We intentionally removed the previous "wait for start event to resolve executionId" logic.
613
+ // Cloud already provides a stable `payload.executionId` and expects it back in `command:response`.
614
+ /**
615
+ * Sends a successful command response to the server
616
+ */
617
+ sendResponse(commandId, executionId, result) {
618
+ const response = {
619
+ commandId,
620
+ executionId,
621
+ result
622
+ };
623
+ this.socket.emit("command:response", response);
624
+ const startedAt = this.commandStarts.get(commandId);
625
+ if (this.logger) {
626
+ this.logger.info("Command response sent", {
627
+ commandId,
628
+ executionId,
629
+ durationMs: startedAt ? Number(durationMs(startedAt).toFixed(2)) : void 0,
630
+ result: summarizeValue(result)
631
+ });
632
+ } else {
633
+ console.log(`\u2705 Sent response for command ${commandId}`);
634
+ }
635
+ }
636
+ /**
637
+ * Sends an error response to the server
638
+ */
639
+ sendError(commandId, error) {
640
+ const errorResponse = {
641
+ commandId,
642
+ error
643
+ };
644
+ this.socket.emit("command:error", errorResponse);
645
+ const startedAt = this.commandStarts.get(commandId);
646
+ if (this.logger) {
647
+ this.logger.error("Command error sent", {
648
+ commandId,
649
+ durationMs: startedAt ? Number(durationMs(startedAt).toFixed(2)) : void 0,
650
+ code: error.code,
651
+ message: error.message
652
+ });
653
+ } else {
654
+ console.error(`\u274C Sent error for command ${commandId}:`, error.message);
655
+ }
656
+ }
657
+ /**
658
+ * Gets the stable ID for an agent (id if available, otherwise name).
659
+ */
660
+ getAgentId(agent) {
661
+ return agent.id ?? agent.name;
662
+ }
663
+ /**
664
+ * Gets the description from an agent if available.
665
+ */
666
+ getAgentDescription(agent) {
667
+ const agentWithConfig = agent;
668
+ const config = agentWithConfig.config;
669
+ if (config && typeof config === "object" && "description" in config) {
670
+ return typeof config.description === "string" ? config.description : void 0;
671
+ }
672
+ return void 0;
673
+ }
674
+ /**
675
+ * Builds the Cloud registration payload (RegisterClientDto compatible).
676
+ */
677
+ async buildRegisterPayload(input) {
678
+ const workflowAgents = this.collectAgentsFromWorkflows();
679
+ const agents = [...this.agents];
680
+ for (const agent of workflowAgents) {
681
+ const id = this.getAgentId(agent);
682
+ const exists = agents.some((x) => this.getAgentId(x) === id);
683
+ if (!exists) agents.push(agent);
684
+ }
685
+ const agentTools = this.collectToolsFromAgents(agents);
686
+ const tools = [...this.tools];
687
+ for (const tool of agentTools) {
688
+ if (tools.some((x) => x.id === tool.id)) continue;
689
+ tools.push(tool);
690
+ }
691
+ const resources = [
692
+ ...agents.map((agent) => {
693
+ const runtimeResourceId = this.getAgentId(agent);
694
+ const description = this.getAgentDescription(agent);
695
+ return {
696
+ type: "agent",
697
+ runtimeResourceId,
698
+ displayName: agent.name,
699
+ kind: agent.kind,
700
+ description
701
+ };
702
+ }),
703
+ ...this.workflows.map((workflow) => {
704
+ const definition = input.includeDefinitions ? this.buildWorkflowDefinition(workflow) : void 0;
705
+ return {
706
+ type: "workflow",
707
+ runtimeResourceId: workflow.id,
708
+ displayName: workflow.name ?? workflow.id,
709
+ description: workflow.description,
710
+ ...definition ? { definition } : {}
711
+ };
712
+ }),
713
+ ...tools.map((tool) => {
714
+ const definition = input.includeDefinitions ? this.buildToolDefinition(tool) : void 0;
715
+ return {
716
+ type: "tool",
717
+ runtimeResourceId: tool.id,
718
+ displayName: tool.id,
719
+ description: tool.description,
720
+ ...definition ? { definition } : {}
721
+ };
722
+ })
723
+ ];
724
+ if (input.includeDefinitions) {
725
+ await Promise.all(
726
+ resources.filter((r) => r.type === "agent").map(async (r) => {
727
+ const agent = agents.find((a) => this.getAgentId(a) === r.runtimeResourceId);
728
+ if (!agent) return;
729
+ const def = await this.buildAgentDefinition(agent);
730
+ if (def) r.definition = def;
731
+ })
732
+ );
733
+ }
734
+ return {
735
+ instanceId: this.instanceId,
736
+ // Dedupe by (type + runtimeResourceId) to avoid duplicates when tools are discovered from multiple sources.
737
+ resources: resources.filter((r, idx, arr) => {
738
+ const key = `${r.type}:${r.runtimeResourceId}`;
739
+ return idx === arr.findIndex((x) => `${x.type}:${x.runtimeResourceId}` === key);
740
+ })
741
+ };
742
+ }
743
+ isZodSchema(value) {
744
+ return !!value && typeof value === "object" && "safeParse" in value && typeof value.safeParse === "function";
745
+ }
746
+ isInstructionResolvable(value) {
747
+ return !!value && typeof value === "object" && "resolve" in value && typeof value.resolve === "function";
748
+ }
749
+ /**
750
+ * Agent definition payload for Cloud persistence + UI inspection.
751
+ *
752
+ * IMPORTANT:
753
+ * - Do NOT embed tool schemas here; tools are registered as independent resources.
754
+ * - We only reference tool ids to avoid duplication.
755
+ */
756
+ async buildAgentDefinition(agent) {
757
+ const agentWithConfig = agent;
758
+ const config = agentWithConfig.config;
759
+ if (!config || typeof config !== "object") return void 0;
760
+ const model = config.model;
761
+ const instructions = config.instructions;
762
+ const tools = Array.isArray(config.tools) ? config.tools : [];
763
+ const interceptors = config.interceptors;
764
+ let instructionsText = "";
765
+ if (typeof instructions === "string") {
766
+ instructionsText = instructions;
767
+ } else if (this.isInstructionResolvable(instructions)) {
768
+ instructionsText = await instructions.resolve();
769
+ }
770
+ const requestInterceptorNames = interceptors?.request?.map((fn) => {
771
+ if (typeof fn === "function" && fn.name) {
772
+ return fn.name;
773
+ }
774
+ return "anonymous";
775
+ }) ?? [];
776
+ const responseInterceptorNames = interceptors?.response?.map((fn) => {
777
+ if (typeof fn === "function" && fn.name) {
778
+ return fn.name;
779
+ }
780
+ return "anonymous";
781
+ }) ?? [];
782
+ const modelInfo = model && typeof model === "object" && "providerName" in model && "modelName" in model ? {
783
+ provider: model.providerName,
784
+ model: model.modelName,
785
+ capabilities: model.capabilities
786
+ } : void 0;
787
+ return {
788
+ schemaVersion: 1,
789
+ resourceType: "agent",
790
+ agentId: agent.id ?? agent.name,
791
+ agentName: agent.name,
792
+ kind: agent.kind,
793
+ model: modelInfo,
794
+ instructionsText,
795
+ toolIds: tools.map((t) => t.id),
796
+ interceptors: {
797
+ request: requestInterceptorNames,
798
+ response: responseInterceptorNames
799
+ }
800
+ };
801
+ }
802
+ /**
803
+ * Builds a workflow definition payload suitable for Cloud persistence + UI graph rendering.
804
+ *
805
+ * The base definition comes from `workflow.describe()` (graph nodes/edges).
806
+ * If the workflow was created with an input Zod schema, we also attach `inputSchema`
807
+ * as a JSON Schema object so the UI can render an input form.
808
+ */
809
+ buildWorkflowDefinition(workflow) {
810
+ const workflowWithNodes = workflow;
811
+ if (typeof workflowWithNodes.describe !== "function") return void 0;
812
+ const base = workflowWithNodes.describe();
813
+ if (!base || typeof base !== "object") return void 0;
814
+ const workflowUnknown = workflow;
815
+ const workflowWithConfig = workflowUnknown && typeof workflowUnknown === "object" && "config" in workflowUnknown && typeof workflowUnknown.config === "object" ? workflowUnknown.config?.inputSchema : void 0;
816
+ const inputSchemaCandidate = workflowWithConfig;
817
+ const withSchemas = this.isZodSchema(inputSchemaCandidate) ? { ...base, inputSchema: zodToJsonSchema(inputSchemaCandidate) } : base;
818
+ const nodes = Array.isArray(withSchemas.nodes) ? withSchemas.nodes : void 0;
819
+ const agentIds = Array.isArray(nodes) ? nodes.map((n) => typeof n?.agentId === "string" && n.agentId ? n.agentId : void 0).filter((x) => !!x) : [];
820
+ return {
821
+ ...withSchemas,
822
+ schemaVersion: 1,
823
+ resourceType: "workflow",
824
+ refs: {
825
+ agents: Array.from(new Set(agentIds)),
826
+ tools: [],
827
+ workflows: []
828
+ }
829
+ };
830
+ }
831
+ buildToolDefinition(tool) {
832
+ const inputSchemaCandidate = tool.inputSchema;
833
+ const inputSchema = this.isZodSchema(inputSchemaCandidate) ? zodToJsonSchema(inputSchemaCandidate) : void 0;
834
+ return {
835
+ schemaVersion: 1,
836
+ resourceType: "tool",
837
+ toolId: tool.id,
838
+ description: tool.description,
839
+ ...inputSchema ? { inputSchema } : {},
840
+ outputType: "unknown"
841
+ };
842
+ }
843
+ /**
844
+ * Full registration (discovery) call. This is idempotent on the Cloud side.
845
+ */
846
+ async registerFull() {
847
+ const payload = await this.buildRegisterPayload({ includeDefinitions: true });
848
+ const start = nowMs();
849
+ return new Promise((resolve, reject) => {
850
+ const timeout = setTimeout(() => {
851
+ reject(
852
+ new ConnectionError(
853
+ "Registration timeout: Server did not respond to registration request. Please check your connection and try again."
854
+ )
855
+ );
856
+ }, 1e4);
857
+ this.socket.emit("client:register:full", payload, (response) => {
858
+ clearTimeout(timeout);
859
+ if (response.success) {
860
+ const defCount = payload.resources.filter((r) => !!r.definition).length;
861
+ const agentDefCount = payload.resources.filter(
862
+ (r) => r.type === "agent" && !!r.definition
863
+ ).length;
864
+ const workflowDefCount = payload.resources.filter(
865
+ (r) => r.type === "workflow" && !!r.definition
866
+ ).length;
867
+ const toolDefCount = payload.resources.filter(
868
+ (r) => r.type === "tool" && !!r.definition
869
+ ).length;
870
+ if (this.logger) {
871
+ this.logger.info("Client registered", {
872
+ clientId: this.clientId,
873
+ instanceId: payload.instanceId,
874
+ resources: payload.resources.length,
875
+ definitions: {
876
+ total: defCount,
877
+ agents: agentDefCount,
878
+ workflows: workflowDefCount,
879
+ tools: toolDefCount
880
+ },
881
+ durationMs: Number(durationMs(start).toFixed(2))
882
+ });
883
+ } else {
884
+ console.log(
885
+ `\u2705 Client registered successfully: ${this.clientId} (${payload.instanceId})`
886
+ );
887
+ console.log(` - ${payload.resources.length} resources registered`);
888
+ console.log(
889
+ ` - definitions: total=${defCount}, agents=${agentDefCount}, workflows=${workflowDefCount}, tools=${toolDefCount}`
890
+ );
891
+ }
892
+ resolve();
893
+ return;
894
+ }
895
+ const errorMessage = response.error ?? "Unknown registration error";
896
+ if (errorMessage.includes("authentication") || errorMessage.includes("Unauthorized") || errorMessage.includes("token") || errorMessage.includes("API key")) {
897
+ reject(
898
+ new AuthenticationError(
899
+ `Registration failed: ${errorMessage}. Please check your NEBULAOS_API_KEY environment variable.`
900
+ )
901
+ );
902
+ return;
903
+ }
904
+ reject(
905
+ new ConnectionError(
906
+ `Registration failed: ${errorMessage}. Please check your connection and configuration.`
907
+ )
908
+ );
909
+ });
910
+ });
911
+ }
912
+ startSnapshotLoop() {
913
+ const intervalMs = this.config.snapshotIntervalMs ?? 6e4;
914
+ if (this.snapshotTimer) clearInterval(this.snapshotTimer);
915
+ this.snapshotTimer = setInterval(() => {
916
+ if (!this.socket.connected) return;
917
+ void this.buildRegisterPayload({ includeDefinitions: false }).then((payload) => {
918
+ this.socket.emit("client:snapshot", payload);
919
+ });
920
+ }, intervalMs);
921
+ }
922
+ /**
923
+ * Connects to the Cloud runtime.
924
+ */
925
+ async connect() {
926
+ return new Promise((resolve, reject) => {
927
+ if (this.socket.connected) {
928
+ resolve();
929
+ return;
930
+ }
931
+ this.connectionAttempted = true;
932
+ this.hasAuthenticated = false;
933
+ let resolved = false;
934
+ let authTimeout;
935
+ const timeout = setTimeout(() => {
936
+ if (resolved) return;
937
+ resolved = true;
938
+ this.connectionAttempted = false;
939
+ reject(
940
+ new ConnectionError(
941
+ `Connection timeout after 10 seconds. Please check your NEBULAOS_URL (${this.config.url}) and ensure the server is running.`
942
+ )
943
+ );
944
+ }, 1e4);
945
+ const cleanup = () => {
946
+ clearTimeout(timeout);
947
+ if (authTimeout) clearTimeout(authTimeout);
948
+ this.socket.off("connect", connectHandler);
949
+ this.socket.off("connect_error", errorHandler);
950
+ this.socket.off("disconnect", disconnectHandler);
951
+ this.socket.off("client:online", onlineHandler);
952
+ };
953
+ const onlineHandler = () => {
954
+ if (resolved) return;
955
+ resolved = true;
956
+ cleanup();
957
+ this.connectionAttempted = false;
958
+ this.hasAuthenticated = true;
959
+ this.startSnapshotLoop();
960
+ resolve();
961
+ };
962
+ const connectHandler = () => {
963
+ clearTimeout(timeout);
964
+ authTimeout = setTimeout(() => {
965
+ if (resolved) return;
966
+ resolved = true;
967
+ cleanup();
968
+ this.connectionAttempted = false;
969
+ reject(
970
+ new AuthenticationError(
971
+ `Connected but authentication was not confirmed by server. Please check your NEBULAOS_API_KEY environment variable.`
972
+ )
973
+ );
974
+ }, 2500);
975
+ };
976
+ const disconnectHandler = (reason) => {
977
+ if (resolved) return;
978
+ if (this.connectionAttempted && reason === "io server disconnect") {
979
+ resolved = true;
980
+ cleanup();
981
+ this.connectionAttempted = false;
982
+ if (this.lastConnectError) {
983
+ const errorMsg = this.lastConnectError.message.toLowerCase();
984
+ if (errorMsg.includes("authorization") || errorMsg.includes("token") || errorMsg.includes("unauthorized") || errorMsg.includes("authentication") || errorMsg.includes("api key") || errorMsg.includes("invalid")) {
985
+ reject(
986
+ new AuthenticationError(
987
+ `Authentication failed: Invalid API key or missing credentials. Please check your NEBULAOS_API_KEY environment variable. (Error: ${this.lastConnectError.message})`
988
+ )
989
+ );
990
+ return;
991
+ }
992
+ }
993
+ reject(
994
+ new AuthenticationError(
995
+ `Connection rejected by server: Invalid API key or missing credentials. Please check your NEBULAOS_API_KEY environment variable. (Disconnect reason: ${reason})`
996
+ )
997
+ );
998
+ return;
999
+ }
1000
+ };
1001
+ const errorHandler = (error) => {
1002
+ if (resolved) return;
1003
+ const errorMsg = error.message.toLowerCase();
1004
+ if (errorMsg.includes("authorization") || errorMsg.includes("token") || errorMsg.includes("unauthorized") || errorMsg.includes("authentication") || errorMsg.includes("api key") || errorMsg.includes("invalid")) {
1005
+ resolved = true;
1006
+ cleanup();
1007
+ this.connectionAttempted = false;
1008
+ reject(
1009
+ new AuthenticationError(
1010
+ `Authentication failed: Invalid API key or missing credentials. Please check your NEBULAOS_API_KEY environment variable. (Server error: ${error.message})`
1011
+ )
1012
+ );
1013
+ return;
1014
+ }
1015
+ if (errorMsg.includes("timeout") || errorMsg.includes("econnrefused")) {
1016
+ resolved = true;
1017
+ cleanup();
1018
+ this.connectionAttempted = false;
1019
+ reject(
1020
+ new ConnectionError(
1021
+ `Connection failed: Unable to reach NebulaOS Cloud at ${this.config.url}. Please check your NEBULAOS_URL environment variable and ensure the server is running. (Error: ${error.message})`
1022
+ )
1023
+ );
1024
+ return;
1025
+ }
1026
+ setTimeout(() => {
1027
+ if (!resolved) {
1028
+ resolved = true;
1029
+ cleanup();
1030
+ this.connectionAttempted = false;
1031
+ reject(
1032
+ new ConnectionError(
1033
+ `Connection error: ${error.message}. Please check your NEBULAOS_URL (${this.config.url}) and NEBULAOS_API_KEY.`
1034
+ )
1035
+ );
1036
+ }
1037
+ }, 1e3);
1038
+ };
1039
+ this.socket.once("connect", connectHandler);
1040
+ this.socket.once("connect_error", errorHandler);
1041
+ this.socket.once("disconnect", disconnectHandler);
1042
+ this.socket.once("client:online", onlineHandler);
1043
+ this.socket.connect();
1044
+ });
1045
+ }
1046
+ /**
1047
+ * Disconnects from the server gracefully
1048
+ */
1049
+ async disconnect() {
1050
+ console.log("\u{1F6D1} Disconnecting from NebulaOS Cloud...");
1051
+ if (this.snapshotTimer) clearInterval(this.snapshotTimer);
1052
+ this.snapshotTimer = void 0;
1053
+ if (this.socket.connected) {
1054
+ await this.sendShutdownNotice({ reason: "disconnect()" });
1055
+ this.socket.disconnect();
1056
+ }
1057
+ console.log("\u{1F44B} Disconnected");
1058
+ }
1059
+ /**
1060
+ * Best-effort shutdown notice to allow the Cloud to immediately clean up instance availability.
1061
+ * Never throws: disconnect must be resilient.
1062
+ */
1063
+ async sendShutdownNotice(input) {
1064
+ const payload = {
1065
+ instanceId: this.instanceId,
1066
+ ...input.reason ? { reason: input.reason } : {}
1067
+ };
1068
+ await new Promise((resolve) => {
1069
+ const timeout = setTimeout(() => resolve(), 1500);
1070
+ try {
1071
+ this.socket.emit("client:shutdown", payload, (_response) => {
1072
+ clearTimeout(timeout);
1073
+ resolve();
1074
+ });
1075
+ } catch {
1076
+ clearTimeout(timeout);
1077
+ resolve();
1078
+ }
1079
+ });
1080
+ }
1081
+ /**
1082
+ * Checks if the client is connected
1083
+ */
1084
+ isConnected() {
1085
+ return this.socket.connected;
1086
+ }
1087
+ /**
1088
+ * Returns the client ID
1089
+ */
1090
+ getClientId() {
1091
+ return this.clientId;
1092
+ }
1093
+ getInstanceId() {
1094
+ return this.instanceId;
1095
+ }
1096
+ /**
1097
+ * Returns the socket instance for telemetry
1098
+ */
1099
+ getSocket() {
1100
+ return this.socket;
1101
+ }
1102
+ };
1103
+
1104
+ // src/client.ts
1105
+ import { ConsoleLogger, DomainEvents, Tracing as Tracing3 } from "@nebulaos/core";
1106
+
1107
+ // src/telemetry/http-telemetry-exporter.ts
1108
+ import axios from "axios";
1109
+ var HttpTelemetryExporter = class {
1110
+ constructor(input, config = {}, logger) {
1111
+ this.input = input;
1112
+ this.logger = logger;
1113
+ this.baseUrl = input.cloudUrl.replace(/\/+$/, "");
1114
+ this.timeoutMs = config.timeoutMs ?? 2e3;
1115
+ }
1116
+ baseUrl;
1117
+ timeoutMs;
1118
+ async exportBatch(events) {
1119
+ for (const event of events) {
1120
+ await this.sendSingle(event);
1121
+ }
1122
+ }
1123
+ async sendSingle(event) {
1124
+ const url = `${this.baseUrl}/telemetry/events`;
1125
+ const start = nowMs();
1126
+ try {
1127
+ const res = await axios.post(
1128
+ url,
1129
+ { events: [event] },
1130
+ {
1131
+ timeout: this.timeoutMs,
1132
+ headers: {
1133
+ "Content-Type": "application/json",
1134
+ ...this.input.apiKey ? { Authorization: `Bearer ${this.input.apiKey}` } : {}
1135
+ },
1136
+ // Telemetry should never throw due to non-2xx.
1137
+ validateStatus: () => true
1138
+ }
1139
+ );
1140
+ if (this.logger) {
1141
+ this.logger.debug(
1142
+ "HTTP telemetry sent",
1143
+ {
1144
+ method: "POST",
1145
+ path: "/telemetry/events",
1146
+ status: res.status,
1147
+ durationMs: Number(durationMs(start).toFixed(2)),
1148
+ timeoutMs: this.timeoutMs,
1149
+ event: summarizeTelemetryEvent(event)
1150
+ }
1151
+ );
1152
+ }
1153
+ } catch (err) {
1154
+ if (this.logger) {
1155
+ const message = err instanceof Error ? err.message : String(err);
1156
+ this.logger.warn(
1157
+ "HTTP telemetry failed (best-effort)",
1158
+ {
1159
+ method: "POST",
1160
+ path: "/telemetry/events",
1161
+ durationMs: Number(durationMs(start).toFixed(2)),
1162
+ timeoutMs: this.timeoutMs,
1163
+ error: message,
1164
+ event: summarizeTelemetryEvent(event)
1165
+ }
1166
+ );
1167
+ }
1168
+ }
1169
+ }
1170
+ };
1171
+
1172
+ // src/domain-events/http-domain-events-exporter.ts
1173
+ import axios2 from "axios";
1174
+ var HttpDomainEventsExporter = class {
1175
+ constructor(input, config = {}, logger) {
1176
+ this.input = input;
1177
+ this.logger = logger;
1178
+ this.baseUrl = input.cloudUrl.replace(/\/+$/, "");
1179
+ this.timeoutMs = config.timeoutMs ?? 2e3;
1180
+ }
1181
+ baseUrl;
1182
+ timeoutMs;
1183
+ async exportBatch(events) {
1184
+ if (events.length === 0) return;
1185
+ const url = `${this.baseUrl}/execution/events`;
1186
+ const start = nowMs();
1187
+ try {
1188
+ const res = await axios2.post(
1189
+ url,
1190
+ { events },
1191
+ {
1192
+ timeout: this.timeoutMs,
1193
+ headers: {
1194
+ "Content-Type": "application/json",
1195
+ ...this.input.apiKey ? { Authorization: `Bearer ${this.input.apiKey}` } : {}
1196
+ },
1197
+ // Domain events should never throw due to non-2xx.
1198
+ validateStatus: () => true
1199
+ }
1200
+ );
1201
+ if (this.logger) {
1202
+ this.logger.debug(
1203
+ "HTTP domain events sent",
1204
+ {
1205
+ method: "POST",
1206
+ path: "/execution/events",
1207
+ status: res.status,
1208
+ durationMs: Number(durationMs(start).toFixed(2)),
1209
+ timeoutMs: this.timeoutMs,
1210
+ batch: summarizeDomainBatch(events),
1211
+ sample: events.slice(0, 3).map((e) => summarizeDomainEvent(e))
1212
+ }
1213
+ );
1214
+ }
1215
+ } catch {
1216
+ if (this.logger) {
1217
+ this.logger.warn(
1218
+ "HTTP domain events failed (best-effort)",
1219
+ {
1220
+ method: "POST",
1221
+ path: "/execution/events",
1222
+ durationMs: Number(durationMs(start).toFixed(2)),
1223
+ timeoutMs: this.timeoutMs,
1224
+ batch: summarizeDomainBatch(events),
1225
+ sample: events.slice(0, 3).map((e) => summarizeDomainEvent(e))
1226
+ }
1227
+ );
1228
+ }
1229
+ }
1230
+ }
1231
+ };
1232
+
1233
+ // src/logger/client-debug.ts
1234
+ function parseBool(value) {
1235
+ if (!value) return false;
1236
+ const v = value.trim().toLowerCase();
1237
+ return v === "1" || v === "true" || v === "yes" || v === "on";
1238
+ }
1239
+ function resolveClientDebugConfig(input) {
1240
+ const env = process.env.NEBULAOS_CLIENT_DEBUG?.trim().toLowerCase();
1241
+ if (env) {
1242
+ if (env === "off" || env === "0" || env === "false") {
1243
+ return { enabled: false, level: "none" };
1244
+ }
1245
+ if (env === "debug") {
1246
+ return { enabled: true, level: "debug" };
1247
+ }
1248
+ if (env === "info") {
1249
+ return { enabled: true, level: "info" };
1250
+ }
1251
+ if (parseBool(env)) {
1252
+ return { enabled: true, level: "debug" };
1253
+ }
1254
+ }
1255
+ if (input?.logLevel) {
1256
+ return {
1257
+ enabled: input.logLevel !== "none",
1258
+ level: input.logLevel
1259
+ };
1260
+ }
1261
+ const enabled = input?.enabled ?? false;
1262
+ const level = input?.level ?? (enabled ? "debug" : "none");
1263
+ return { enabled, level };
1264
+ }
1265
+
1266
+ // src/client.ts
1267
+ import { v4 as uuid2 } from "uuid";
1268
+
1269
+ // src/tracing/setup.ts
1270
+ import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
1271
+ import { Resource } from "@opentelemetry/resources";
1272
+ import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
1273
+ import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
1274
+ import { registerInstrumentations } from "@opentelemetry/instrumentation";
1275
+ function setupOTelTracing(config) {
1276
+ const resource = new Resource({
1277
+ "service.name": config.serviceName
1278
+ });
1279
+ const provider = new BasicTracerProvider({ resource });
1280
+ provider.register({
1281
+ contextManager: new AsyncLocalStorageContextManager()
1282
+ });
1283
+ registerInstrumentations({
1284
+ tracerProvider: provider,
1285
+ instrumentations: [
1286
+ new HttpInstrumentation()
1287
+ ]
1288
+ });
1289
+ }
1290
+
1291
+ // src/tracing/otel-provider.ts
1292
+ import { context, trace, SpanStatusCode } from "@opentelemetry/api";
1293
+ import {
1294
+ ActiveSpan
1295
+ } from "@nebulaos/core";
1296
+ var OTelTracingProvider = class {
1297
+ constructor(serviceName, exporter) {
1298
+ this.exporter = exporter;
1299
+ this.tracer = trace.getTracer(serviceName);
1300
+ }
1301
+ tracer;
1302
+ /**
1303
+ * Ensures an OTel ID string is in W3C lowercase hex format.
1304
+ *
1305
+ * If the value is already valid hex of the expected length it is returned
1306
+ * as-is. Otherwise we assume it is base64-encoded and convert it.
1307
+ *
1308
+ * @param id Raw ID from OTel spanContext()
1309
+ * @param hexLen Expected hex length (16 for spanId, 32 for traceId)
1310
+ */
1311
+ ensureW3cHex(id, hexLen) {
1312
+ if (!id) return void 0;
1313
+ const hexRegex = hexLen === 16 ? /^[0-9a-f]{16}$/ : /^[0-9a-f]{32}$/;
1314
+ if (hexRegex.test(id)) return id;
1315
+ try {
1316
+ const hex = Buffer.from(id, "base64").toString("hex");
1317
+ return hex.slice(0, hexLen).padStart(hexLen, "0");
1318
+ } catch {
1319
+ return void 0;
1320
+ }
1321
+ }
1322
+ startSpan(input) {
1323
+ const parentCtx = context.active();
1324
+ const parentSpan = trace.getSpan(parentCtx);
1325
+ const otelSpan = this.tracer.startSpan(
1326
+ `${input.kind}:${input.name}`,
1327
+ {
1328
+ attributes: {
1329
+ "nebula.span.kind": input.kind,
1330
+ "nebula.span.name": input.name,
1331
+ ...input.correlationId ? { "nebula.correlationId": input.correlationId } : {},
1332
+ ...input.executionId ? { "nebula.executionId": input.executionId } : {}
1333
+ }
1334
+ },
1335
+ parentCtx
1336
+ );
1337
+ const spanContext = otelSpan.spanContext();
1338
+ const traceId = this.ensureW3cHex(spanContext.traceId, 32) ?? spanContext.traceId;
1339
+ const spanId = this.ensureW3cHex(spanContext.spanId, 16) ?? spanContext.spanId;
1340
+ const parentSpanId = this.ensureW3cHex(parentSpan?.spanContext().spanId, 16);
1341
+ const activeSpan = new OTelActiveSpan(
1342
+ traceId,
1343
+ spanId,
1344
+ parentSpanId,
1345
+ input.kind,
1346
+ input.name,
1347
+ input.correlationId,
1348
+ input.executionId,
1349
+ this.exporter,
1350
+ otelSpan
1351
+ );
1352
+ const startEvent = {
1353
+ v: 1,
1354
+ type: "telemetry:span:start",
1355
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1356
+ trace: { traceId, spanId, parentSpanId },
1357
+ correlationId: input.correlationId,
1358
+ executionId: input.executionId,
1359
+ span: {
1360
+ kind: input.kind,
1361
+ name: input.name,
1362
+ data: input.data ?? {}
1363
+ }
1364
+ };
1365
+ this.exporter.exportBatch([startEvent]).catch(() => {
1366
+ });
1367
+ return activeSpan;
1368
+ }
1369
+ getContext() {
1370
+ const span = trace.getSpan(context.active());
1371
+ if (!span) return void 0;
1372
+ const sc = span.spanContext();
1373
+ return {
1374
+ traceId: this.ensureW3cHex(sc.traceId, 32) ?? sc.traceId,
1375
+ spanId: this.ensureW3cHex(sc.spanId, 16) ?? sc.spanId
1376
+ };
1377
+ }
1378
+ async withSpan(input, fn) {
1379
+ const parentCtx = context.active();
1380
+ const parentSpan = trace.getSpan(parentCtx);
1381
+ const otelSpan = this.tracer.startSpan(
1382
+ `${input.kind}:${input.name}`,
1383
+ {
1384
+ attributes: {
1385
+ "nebula.span.kind": input.kind,
1386
+ "nebula.span.name": input.name,
1387
+ ...input.correlationId ? { "nebula.correlationId": input.correlationId } : {},
1388
+ ...input.executionId ? { "nebula.executionId": input.executionId } : {}
1389
+ }
1390
+ },
1391
+ parentCtx
1392
+ );
1393
+ const spanContext = otelSpan.spanContext();
1394
+ const traceId = this.ensureW3cHex(spanContext.traceId, 32) ?? spanContext.traceId;
1395
+ const spanId = this.ensureW3cHex(spanContext.spanId, 16) ?? spanContext.spanId;
1396
+ const parentSpanId = this.ensureW3cHex(parentSpan?.spanContext().spanId, 16);
1397
+ const activeSpan = new OTelActiveSpan(
1398
+ traceId,
1399
+ spanId,
1400
+ parentSpanId,
1401
+ input.kind,
1402
+ input.name,
1403
+ input.correlationId,
1404
+ input.executionId,
1405
+ this.exporter,
1406
+ otelSpan
1407
+ );
1408
+ const startEvent = {
1409
+ v: 1,
1410
+ type: "telemetry:span:start",
1411
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1412
+ trace: {
1413
+ traceId,
1414
+ spanId,
1415
+ parentSpanId
1416
+ },
1417
+ correlationId: input.correlationId,
1418
+ executionId: input.executionId,
1419
+ span: {
1420
+ kind: input.kind,
1421
+ name: input.name,
1422
+ data: input.data ?? {}
1423
+ }
1424
+ };
1425
+ try {
1426
+ await this.exporter.exportBatch([startEvent]);
1427
+ } catch {
1428
+ }
1429
+ const ctx = trace.setSpan(parentCtx, otelSpan);
1430
+ return context.with(ctx, async () => {
1431
+ try {
1432
+ const result = await fn(activeSpan);
1433
+ if (!activeSpan.isEnded) {
1434
+ await activeSpan.end({ status: "success" });
1435
+ }
1436
+ return result;
1437
+ } catch (error) {
1438
+ await activeSpan.end({ status: "error" });
1439
+ throw error;
1440
+ }
1441
+ });
1442
+ }
1443
+ };
1444
+ var OTelActiveSpan = class extends ActiveSpan {
1445
+ constructor(traceId, spanId, parentSpanId, kind, name, correlationId, executionId, exporter, otelSpan) {
1446
+ super(traceId, spanId, parentSpanId, kind, name, correlationId, executionId, exporter);
1447
+ this.otelSpan = otelSpan;
1448
+ }
1449
+ async end(input) {
1450
+ if (this.isEnded) return;
1451
+ if (input.status === "error") {
1452
+ this.otelSpan.setStatus({ code: SpanStatusCode.ERROR });
1453
+ } else {
1454
+ this.otelSpan.setStatus({ code: SpanStatusCode.OK });
1455
+ }
1456
+ this.otelSpan.end();
1457
+ await super.end(input);
1458
+ }
1459
+ };
1460
+
1461
+ // src/http/instrumented-http-client.ts
1462
+ import { Tracing as Tracing2 } from "@nebulaos/core";
1463
+ var InstrumentedHttpClient = class {
1464
+ async fetch(url, options) {
1465
+ const method = options?.method || "GET";
1466
+ const spanName = `http:${method} ${new URL(url).origin}`;
1467
+ return Tracing2.withSpan(
1468
+ {
1469
+ kind: "http",
1470
+ name: spanName,
1471
+ data: {
1472
+ "http.method": method,
1473
+ "http.url": url
1474
+ }
1475
+ },
1476
+ async (span) => {
1477
+ const startTime = Date.now();
1478
+ try {
1479
+ const response = await fetch(url, options);
1480
+ const contentType = response.headers.get("content-type") || "";
1481
+ let data;
1482
+ if (contentType.includes("application/json")) {
1483
+ data = await response.json();
1484
+ } else {
1485
+ data = await response.text();
1486
+ }
1487
+ await span.end({
1488
+ status: response.ok ? "success" : "error",
1489
+ data: {
1490
+ "http.status_code": response.status,
1491
+ "http.response_content_length": response.headers.get("content-length"),
1492
+ "http.duration_ms": Date.now() - startTime
1493
+ }
1494
+ });
1495
+ return {
1496
+ status: response.status,
1497
+ statusText: response.statusText,
1498
+ headers: response.headers,
1499
+ data
1500
+ };
1501
+ } catch (error) {
1502
+ await span.end({
1503
+ status: "error",
1504
+ data: {
1505
+ "http.error": error instanceof Error ? error.message : String(error),
1506
+ "http.duration_ms": Date.now() - startTime
1507
+ }
1508
+ });
1509
+ throw error;
1510
+ }
1511
+ }
1512
+ );
1513
+ }
1514
+ async get(url, options) {
1515
+ return this.fetch(url, { ...options, method: "GET" });
1516
+ }
1517
+ async post(url, body, options) {
1518
+ return this.fetch(url, {
1519
+ ...options,
1520
+ method: "POST",
1521
+ body: body ? JSON.stringify(body) : void 0,
1522
+ headers: { "Content-Type": "application/json", ...options?.headers }
1523
+ });
1524
+ }
1525
+ };
1526
+
1527
+ // src/client.ts
1528
+ var NebulaClient = class {
1529
+ constructor(config) {
1530
+ this.config = config;
1531
+ clientConfigSchema.parse(config);
1532
+ if (!config.clientId) {
1533
+ config.clientId = `client-${uuid2()}`;
1534
+ }
1535
+ if (config.server) {
1536
+ this.setGlobalClientContext(config.server.apiKey, config.server.url);
1537
+ }
1538
+ this.registry = new Registry();
1539
+ this.registry.setHttpClient(new InstrumentedHttpClient());
1540
+ config.agents?.forEach((agent) => this.registry.registerAgent(agent));
1541
+ config.tools?.forEach((tool) => this.registry.registerTool(tool));
1542
+ config.workflows?.forEach((workflow) => this.registry.registerWorkflow(workflow));
1543
+ if (config.server) {
1544
+ const debug = resolveClientDebugConfig({
1545
+ logLevel: config.logLevel,
1546
+ // deprecated:
1547
+ enabled: config.debug?.enabled,
1548
+ level: config.debug?.level
1549
+ });
1550
+ if (debug.enabled) {
1551
+ this.logger = new ConsoleLogger(debug.level, "nebulaos/client");
1552
+ this.logger.info(
1553
+ "Client debug enabled",
1554
+ { level: debug.level, ws: true, http: true }
1555
+ );
1556
+ }
1557
+ this.serverConnection = new ServerConnection({
1558
+ url: config.server.url,
1559
+ apiKey: config.server.apiKey,
1560
+ clientId: config.clientId
1561
+ }, this.logger);
1562
+ this.serverConnection.setRegistry(this.registry);
1563
+ this.telemetryExporter = new HttpTelemetryExporter(
1564
+ {
1565
+ cloudUrl: config.server.url,
1566
+ apiKey: config.server.apiKey,
1567
+ clientId: config.clientId
1568
+ },
1569
+ {
1570
+ timeoutMs: 2e3
1571
+ },
1572
+ this.logger
1573
+ );
1574
+ Tracing3.setExporter(this.telemetryExporter);
1575
+ setupOTelTracing({
1576
+ serviceName: config.clientId ?? "nebulaos-client"
1577
+ });
1578
+ const otelProvider = new OTelTracingProvider(
1579
+ config.clientId ?? "nebulaos-client",
1580
+ this.telemetryExporter
1581
+ );
1582
+ Tracing3.setProvider(otelProvider);
1583
+ this.domainEventsExporter = new HttpDomainEventsExporter(
1584
+ {
1585
+ cloudUrl: config.server.url,
1586
+ apiKey: config.server.apiKey,
1587
+ clientId: config.clientId
1588
+ },
1589
+ {
1590
+ timeoutMs: 2e3
1591
+ },
1592
+ this.logger
1593
+ );
1594
+ DomainEvents.setExporter(this.domainEventsExporter);
1595
+ }
1596
+ }
1597
+ registry;
1598
+ serverConnection;
1599
+ telemetryExporter;
1600
+ domainEventsExporter;
1601
+ logger;
1602
+ registerAgent(agent) {
1603
+ this.registry.registerAgent(agent);
1604
+ }
1605
+ registerTool(tool) {
1606
+ this.registry.registerTool(tool);
1607
+ }
1608
+ registerWorkflow(workflow) {
1609
+ this.registry.registerWorkflow(workflow);
1610
+ }
1611
+ async start() {
1612
+ if (this.serverConnection) {
1613
+ this.serverConnection.registerResources(
1614
+ this.registry.listAgents(),
1615
+ this.registry.listTools(),
1616
+ this.registry.listWorkflows()
1617
+ );
1618
+ try {
1619
+ await this.serverConnection.connect();
1620
+ } catch (error) {
1621
+ if (error instanceof Error) {
1622
+ throw error;
1623
+ }
1624
+ throw new Error(`Failed to connect to NebulaOS Cloud: ${String(error)}`);
1625
+ }
1626
+ }
1627
+ }
1628
+ async stop() {
1629
+ await this.serverConnection?.disconnect();
1630
+ }
1631
+ getClientId() {
1632
+ return this.serverConnection?.getClientId();
1633
+ }
1634
+ isConnected() {
1635
+ return this.serverConnection?.isConnected() ?? false;
1636
+ }
1637
+ /**
1638
+ * Sets global client context for skills to access API key and server URL
1639
+ * This allows skills like RagOpenAISkill to automatically use the client's credentials
1640
+ */
1641
+ setGlobalClientContext(apiKey, serverUrl) {
1642
+ try {
1643
+ import("@nebulaos/rag-openai-skill").then((module) => {
1644
+ if (module.ClientContext) {
1645
+ module.ClientContext.setApiKey(apiKey);
1646
+ module.ClientContext.setServerUrl(serverUrl);
1647
+ }
1648
+ }).catch(() => {
1649
+ });
1650
+ } catch {
1651
+ }
1652
+ }
1653
+ };
1654
+
1655
+ // src/telemetry/telemetry.service.ts
1656
+ var TelemetryService = class {
1657
+ };
1658
+
1659
+ // src/telemetry/batcher.ts
1660
+ var Batcher = class {
1661
+ };
1662
+
1663
+ // src/telemetry/socket-telemetry-exporter.ts
1664
+ var SocketTelemetryExporter = class {
1665
+ constructor(serverConnection, config = {}) {
1666
+ this.serverConnection = serverConnection;
1667
+ this.config = config;
1668
+ }
1669
+ queue = [];
1670
+ flushTimer;
1671
+ flushing = false;
1672
+ async exportBatch(events) {
1673
+ this.queue.push(...events);
1674
+ this.ensureTimer();
1675
+ await this.flushIfNeeded();
1676
+ }
1677
+ async flush() {
1678
+ if (this.flushing) return;
1679
+ if (this.queue.length === 0) return;
1680
+ this.flushing = true;
1681
+ try {
1682
+ const maxBatchSize = this.config.maxBatchSize ?? 50;
1683
+ while (this.queue.length > 0) {
1684
+ const batch = this.queue.splice(0, maxBatchSize);
1685
+ await this.sendTelemetryBatch(batch);
1686
+ }
1687
+ } finally {
1688
+ this.flushing = false;
1689
+ }
1690
+ }
1691
+ stop() {
1692
+ if (this.flushTimer) clearInterval(this.flushTimer);
1693
+ this.flushTimer = void 0;
1694
+ }
1695
+ async sendTelemetryBatch(events) {
1696
+ if (!this.serverConnection.isConnected()) {
1697
+ return;
1698
+ }
1699
+ const socket = this.serverConnection.getSocket();
1700
+ socket.emit("telemetry:batch", {
1701
+ clientId: this.serverConnection.getClientId(),
1702
+ events
1703
+ });
1704
+ }
1705
+ ensureTimer() {
1706
+ if (this.flushTimer) return;
1707
+ const flushIntervalMs = this.config.flushIntervalMs ?? 500;
1708
+ this.flushTimer = setInterval(() => {
1709
+ void this.flush();
1710
+ }, flushIntervalMs);
1711
+ }
1712
+ async flushIfNeeded() {
1713
+ const maxBatchSize = this.config.maxBatchSize ?? 50;
1714
+ if (this.queue.length >= maxBatchSize) {
1715
+ await this.flush();
1716
+ }
1717
+ }
1718
+ };
1719
+
1720
+ // src/memory/db-memory.ts
1721
+ var DBMemory = class {
1722
+ async get(_key, _sessionId) {
1723
+ return null;
1724
+ }
1725
+ async set(_key, _value, _sessionId, _ttl) {
1726
+ }
1727
+ async delete(_key, _sessionId) {
1728
+ }
1729
+ async clear(_sessionId) {
1730
+ }
1731
+ async getHistory(_sessionId, _limit) {
1732
+ return [];
1733
+ }
1734
+ };
1735
+
1736
+ // src/prompts/prompt-capture.ts
1737
+ var PromptCapture = class {
1738
+ };
1739
+
1740
+ // src/rag/rag-client.ts
1741
+ var RAGClient = class {
1742
+ };
1743
+ export {
1744
+ AuthenticationError,
1745
+ Batcher,
1746
+ ClientError,
1747
+ ConnectionError,
1748
+ DBMemory,
1749
+ HttpDomainEventsExporter,
1750
+ HttpTelemetryExporter,
1751
+ InstrumentedHttpClient,
1752
+ NebulaClient,
1753
+ NotFoundError,
1754
+ OTelTracingProvider,
1755
+ PromptCapture,
1756
+ RAGClient,
1757
+ RegistryError,
1758
+ SocketTelemetryExporter,
1759
+ TelemetryService,
1760
+ clientConfigSchema,
1761
+ otelTracingConfigSchema,
1762
+ serverConfigSchema,
1763
+ setupOTelTracing
1764
+ };
1765
+ //# sourceMappingURL=index.mjs.map