@lobu/core 3.0.12 → 3.0.13

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/src/errors.ts ADDED
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Base error class for all lobu errors
3
+ */
4
+ export abstract class BaseError extends Error {
5
+ abstract readonly name: string;
6
+ public operation?: string;
7
+
8
+ constructor(
9
+ message: string,
10
+ public cause?: Error
11
+ ) {
12
+ super(message);
13
+
14
+ // Maintain proper prototype chain for instanceof checks
15
+ Object.setPrototypeOf(this, new.target.prototype);
16
+ }
17
+
18
+ /**
19
+ * Get the full error chain as a string
20
+ */
21
+ getFullMessage(): string {
22
+ let message = `${this.name}: ${this.message}`;
23
+ if (this.cause) {
24
+ if (this.cause instanceof BaseError) {
25
+ message += `\nCaused by: ${this.cause.getFullMessage()}`;
26
+ } else {
27
+ message += `\nCaused by: ${this.cause.message}`;
28
+ }
29
+ }
30
+ return message;
31
+ }
32
+
33
+ /**
34
+ * Convert error to JSON for logging/serialization
35
+ */
36
+ toJSON(): Record<string, any> {
37
+ return {
38
+ name: this.name,
39
+ message: this.message,
40
+ ...(this.operation && { operation: this.operation }),
41
+ cause:
42
+ this.cause instanceof BaseError
43
+ ? this.cause.toJSON()
44
+ : this.cause?.message,
45
+ stack: this.stack,
46
+ };
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Error class for worker-related operations
52
+ */
53
+ export class WorkerError extends BaseError {
54
+ override readonly name = "WorkerError";
55
+
56
+ constructor(operation: string, message: string, cause?: Error) {
57
+ super(message, cause);
58
+ this.operation = operation;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Error class for workspace-related operations
64
+ */
65
+ export class WorkspaceError extends BaseError {
66
+ override readonly name = "WorkspaceError";
67
+
68
+ constructor(operation: string, message: string, cause?: Error) {
69
+ super(message, cause);
70
+ this.operation = operation;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Error class for platform-related operations (Slack, WhatsApp, etc.)
76
+ */
77
+ export class PlatformError extends BaseError {
78
+ override readonly name = "PlatformError";
79
+
80
+ constructor(
81
+ public platform: string,
82
+ operation: string,
83
+ message: string,
84
+ cause?: Error
85
+ ) {
86
+ super(message, cause);
87
+ this.operation = operation;
88
+ }
89
+
90
+ override toJSON(): Record<string, any> {
91
+ return {
92
+ ...super.toJSON(),
93
+ platform: this.platform,
94
+ };
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Error class for session-related operations
100
+ */
101
+ export class SessionError extends BaseError {
102
+ readonly name = "SessionError";
103
+
104
+ constructor(
105
+ public sessionKey: string,
106
+ public code: string,
107
+ message: string,
108
+ cause?: Error
109
+ ) {
110
+ super(message, cause);
111
+ }
112
+
113
+ toJSON(): Record<string, any> {
114
+ return {
115
+ ...super.toJSON(),
116
+ sessionKey: this.sessionKey,
117
+ code: this.code,
118
+ };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Worker error variant with workerId for core operations
124
+ */
125
+ export class CoreWorkerError extends WorkerError {
126
+ constructor(
127
+ public workerId: string,
128
+ operation: string,
129
+ message: string,
130
+ cause?: Error
131
+ ) {
132
+ super(operation, message, cause);
133
+ }
134
+
135
+ override toJSON(): Record<string, any> {
136
+ return {
137
+ ...super.toJSON(),
138
+ workerId: this.workerId,
139
+ };
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Error class for dispatcher-related operations
145
+ */
146
+ export class DispatcherError extends BaseError {
147
+ override readonly name = "DispatcherError";
148
+
149
+ constructor(operation: string, message: string, cause?: Error) {
150
+ super(message, cause);
151
+ this.operation = operation;
152
+ }
153
+ }
154
+
155
+ // ErrorCode enum for orchestration operations
156
+ export enum ErrorCode {
157
+ DATABASE_CONNECTION_FAILED = "DATABASE_CONNECTION_FAILED",
158
+ KUBERNETES_API_ERROR = "KUBERNETES_API_ERROR",
159
+ DEPLOYMENT_SCALE_FAILED = "DEPLOYMENT_SCALE_FAILED",
160
+ DEPLOYMENT_CREATE_FAILED = "DEPLOYMENT_CREATE_FAILED",
161
+ DEPLOYMENT_DELETE_FAILED = "DEPLOYMENT_DELETE_FAILED",
162
+ QUEUE_JOB_PROCESSING_FAILED = "QUEUE_JOB_PROCESSING_FAILED",
163
+ USER_CREDENTIALS_CREATE_FAILED = "USER_CREDENTIALS_CREATE_FAILED",
164
+ INVALID_CONFIGURATION = "INVALID_CONFIGURATION",
165
+ THREAD_DEPLOYMENT_NOT_FOUND = "THREAD_DEPLOYMENT_NOT_FOUND",
166
+ USER_QUEUE_NOT_FOUND = "USER_QUEUE_NOT_FOUND",
167
+ }
168
+
169
+ /**
170
+ * Error class for orchestrator-related operations
171
+ */
172
+ export class OrchestratorError extends BaseError {
173
+ readonly name = "OrchestratorError";
174
+
175
+ constructor(
176
+ public code: ErrorCode,
177
+ message: string,
178
+ public details?: any,
179
+ public shouldRetry: boolean = false,
180
+ cause?: Error
181
+ ) {
182
+ super(message, cause);
183
+ }
184
+
185
+ static fromDatabaseError(error: any): OrchestratorError {
186
+ return new OrchestratorError(
187
+ ErrorCode.DATABASE_CONNECTION_FAILED,
188
+ `Database error: ${error instanceof Error ? error.message : String(error)}`,
189
+ { code: error.code, detail: error.detail },
190
+ true,
191
+ error
192
+ );
193
+ }
194
+
195
+ static fromKubernetesError(error: any): OrchestratorError {
196
+ return new OrchestratorError(
197
+ ErrorCode.KUBERNETES_API_ERROR,
198
+ `Kubernetes operation failed: ${error.message}`,
199
+ error,
200
+ true,
201
+ error
202
+ );
203
+ }
204
+
205
+ toJSON(): Record<string, any> {
206
+ return {
207
+ ...super.toJSON(),
208
+ code: this.code,
209
+ details: this.details,
210
+ shouldRetry: this.shouldRetry,
211
+ };
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Error class for configuration-related operations
217
+ */
218
+ export class ConfigError extends BaseError {
219
+ readonly name = "ConfigError";
220
+ }
package/src/index.ts ADDED
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // Shared exports for @lobu/core consumers (gateway, worker, external tools)
4
+
5
+ export * from "./agent-policy";
6
+ // Agent store interface (unified storage abstraction)
7
+ export type {
8
+ AgentAccessStore,
9
+ AgentConfigStore,
10
+ AgentConnectionStore,
11
+ AgentMetadata,
12
+ AgentSettings,
13
+ AgentStore,
14
+ ChannelBinding,
15
+ ConnectionSettings,
16
+ Grant,
17
+ StoredConnection,
18
+ } from "./agent-store";
19
+ export { findTemplateAgentId } from "./agent-store";
20
+ // Agent Settings API response types (for UI consumers)
21
+ export type {
22
+ AgentConfigResponse,
23
+ AgentInfo,
24
+ CatalogProvider,
25
+ Connection,
26
+ McpConfig,
27
+ ModelOption,
28
+ ModelSelectionState,
29
+ PermissionGrant,
30
+ PrefillMcp,
31
+ PrefillSkill,
32
+ ProviderInfo,
33
+ ProviderState,
34
+ ProviderStatus,
35
+ Schedule,
36
+ SettingsSnapshot,
37
+ Skill,
38
+ SkillMcpServerInfo,
39
+ } from "./api-types";
40
+ export type { CommandContext, CommandDefinition } from "./command-registry";
41
+ // Command registry
42
+ export { CommandRegistry } from "./command-registry";
43
+ export * from "./constants";
44
+ // Errors & logging
45
+ export * from "./errors";
46
+ // Integration types
47
+ export type {
48
+ SystemSkillEntry,
49
+ SystemSkillsConfigFile,
50
+ } from "./integration-types";
51
+ export * from "./logger";
52
+ // Module system
53
+ export type { ActionButton, ModuleSessionContext } from "./modules";
54
+ export * from "./modules";
55
+ export type { OtelConfig, Span, Tracer } from "./otel";
56
+ // OpenTelemetry tracing
57
+ export {
58
+ createChildSpan,
59
+ createRootSpan,
60
+ createSpan,
61
+ flushTracing,
62
+ getCurrentSpan,
63
+ getTraceparent,
64
+ getTracer,
65
+ initTracing,
66
+ runInSpanContext,
67
+ SpanKind,
68
+ SpanStatusCode,
69
+ shutdownTracing,
70
+ withChildSpan,
71
+ withSpan,
72
+ } from "./otel";
73
+ // Plugin types
74
+ export type {
75
+ PluginConfig,
76
+ PluginManifest,
77
+ PluginSlot,
78
+ PluginsConfig,
79
+ ProviderRegistration,
80
+ } from "./plugin-types";
81
+ // Config-driven provider types
82
+ export type {
83
+ ConfigProviderMeta,
84
+ ProviderConfigEntry,
85
+ } from "./provider-config-types";
86
+ // Redis & worker helpers
87
+ export * from "./redis/base-store";
88
+ // Observability
89
+ export { getSentry, initSentry } from "./sentry";
90
+ export { extractTraceId, generateTraceId } from "./trace";
91
+ // Core types
92
+ export type {
93
+ AgentMcpConfig,
94
+ AgentOptions,
95
+ AuthProfile,
96
+ CliBackendConfig,
97
+ ConversationMessage,
98
+ HistoryMessage,
99
+ InstalledProvider,
100
+ InstructionContext,
101
+ InstructionProvider,
102
+ LogLevel,
103
+ McpOAuthConfig,
104
+ McpServerConfig,
105
+ NetworkConfig,
106
+ NixConfig,
107
+ RegistryEntry,
108
+ SessionContext,
109
+ SkillConfig,
110
+ SkillMcpServer,
111
+ SkillsConfig,
112
+ SuggestedPrompt,
113
+ ThinkingLevel,
114
+ ThreadResponsePayload,
115
+ ToolsConfig,
116
+ UserSuggestion,
117
+ } from "./types";
118
+
119
+ // Utilities
120
+ export * from "./utils/encryption";
121
+ export * from "./utils/env";
122
+ export * from "./utils/json";
123
+ export * from "./utils/lock";
124
+ export type { McpToolDef } from "./utils/mcp-tool-instructions";
125
+ export * from "./utils/retry";
126
+ export * from "./utils/sanitize";
127
+ export * from "./worker/auth";
128
+ export type {
129
+ WorkerTransport,
130
+ WorkerTransportConfig,
131
+ } from "./worker/transport";
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared types for the integration system.
3
+ *
4
+ * OAuth credential management for third-party APIs (GitHub, Google, etc.)
5
+ * is handled by Owletto.
6
+ */
7
+
8
+ import type { ProviderConfigEntry } from "./provider-config-types";
9
+
10
+ // System Skills Config (config/system-skills.json)
11
+
12
+ export interface SystemSkillEntry {
13
+ id: string;
14
+ name: string;
15
+ description?: string;
16
+ instructions?: string;
17
+ hidden?: boolean;
18
+ mcpServers?: import("./types").SkillMcpServer[];
19
+ providers?: ProviderConfigEntry[];
20
+ nixPackages?: string[];
21
+ permissions?: string[];
22
+ }
23
+
24
+ export interface SystemSkillsConfigFile {
25
+ skills: SystemSkillEntry[];
26
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,248 @@
1
+ // Use simple console.log-based logger by default (unbuffered, 12-factor compliant)
2
+ // Set USE_WINSTON_LOGGER=true only if you need Winston features (file rotation, multiple transports)
3
+ const USE_WINSTON_LOGGER = process.env.USE_WINSTON_LOGGER === "true";
4
+ // Use JSON format for structured logging (better for Loki parsing in production)
5
+ const USE_JSON_FORMAT = process.env.LOG_FORMAT === "json";
6
+
7
+ import winston from "winston";
8
+ import { getSentry } from "./sentry";
9
+
10
+ export interface Logger {
11
+ error: (message: any, ...args: any[]) => void;
12
+ warn: (message: any, ...args: any[]) => void;
13
+ info: (message: any, ...args: any[]) => void;
14
+ debug: (message: any, ...args: any[]) => void;
15
+ }
16
+
17
+ // Simple console logger fallback for environments where Winston doesn't work (Bun + Alpine)
18
+ // Supports both formats: logger.info("message", data) AND pino-style logger.info({ data }, "message")
19
+ function createConsoleLogger(serviceName: string): Logger {
20
+ const level = process.env.LOG_LEVEL || "info";
21
+ const levels: Record<string, number> = {
22
+ error: 0,
23
+ warn: 1,
24
+ info: 2,
25
+ debug: 3,
26
+ };
27
+ const currentLevel = levels[level] ?? 2;
28
+
29
+ const formatMessage = (lvl: string, message: any, ...args: any[]): string => {
30
+ const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
31
+ let msgStr: string;
32
+ let meta: any = null;
33
+
34
+ // Handle pino-style format: logger.info({ key: value }, "message")
35
+ if (
36
+ typeof message === "object" &&
37
+ message !== null &&
38
+ !Array.isArray(message) &&
39
+ !(message instanceof Error)
40
+ ) {
41
+ if (args.length > 0 && typeof args[0] === "string") {
42
+ // First arg is metadata object, second arg is the actual message
43
+ msgStr = args[0];
44
+ meta = message;
45
+ args = args.slice(1);
46
+ } else {
47
+ // Just an object, stringify it
48
+ try {
49
+ msgStr = JSON.stringify(message);
50
+ } catch {
51
+ msgStr = "[object]";
52
+ }
53
+ }
54
+ } else {
55
+ msgStr = String(message);
56
+ }
57
+
58
+ // Append remaining args
59
+ if (args.length > 0) {
60
+ try {
61
+ msgStr += ` ${JSON.stringify(args.length === 1 ? args[0] : args)}`;
62
+ } catch {
63
+ msgStr += " [unserializable]";
64
+ }
65
+ }
66
+
67
+ // Append metadata object
68
+ if (meta) {
69
+ try {
70
+ msgStr += ` ${JSON.stringify(meta)}`;
71
+ } catch {
72
+ msgStr += " [meta unserializable]";
73
+ }
74
+ }
75
+
76
+ return `[${timestamp}] [${lvl}] [${serviceName}] ${msgStr}`;
77
+ };
78
+
79
+ return {
80
+ error: (message: any, ...args: any[]) => {
81
+ if (currentLevel >= 0)
82
+ console.error(formatMessage("error", message, ...args));
83
+ },
84
+ warn: (message: any, ...args: any[]) => {
85
+ if (currentLevel >= 1)
86
+ console.warn(formatMessage("warn", message, ...args));
87
+ },
88
+ info: (message: any, ...args: any[]) => {
89
+ if (currentLevel >= 2)
90
+ console.log(formatMessage("info", message, ...args));
91
+ },
92
+ debug: (message: any, ...args: any[]) => {
93
+ if (currentLevel >= 3)
94
+ console.log(formatMessage("debug", message, ...args));
95
+ },
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Custom Winston transport that sends errors to Sentry
101
+ */
102
+ class SentryTransport extends winston.transports.Stream {
103
+ constructor() {
104
+ super({ stream: process.stdout });
105
+ }
106
+
107
+ log(info: any, callback: () => void) {
108
+ setImmediate(() => {
109
+ this.emit("logged", info);
110
+ });
111
+
112
+ // Only send errors and warnings to Sentry
113
+ if (info.level === "error" || info.level === "warn") {
114
+ const Sentry = getSentry();
115
+ if (Sentry) {
116
+ try {
117
+ // Extract error object if present
118
+ const errorObj =
119
+ info.error || (info.message instanceof Error ? info.message : null);
120
+
121
+ if (errorObj instanceof Error) {
122
+ Sentry.captureException(errorObj, {
123
+ level: info.level === "error" ? "error" : "warning",
124
+ tags: {
125
+ service: info.service,
126
+ },
127
+ extra: {
128
+ ...info,
129
+ message: info.message,
130
+ },
131
+ });
132
+ } else {
133
+ // Send as message if no Error object
134
+ Sentry.captureMessage(String(info.message), {
135
+ level: info.level === "error" ? "error" : "warning",
136
+ tags: {
137
+ service: info.service,
138
+ },
139
+ extra: info,
140
+ });
141
+ }
142
+ } catch (_err) {
143
+ // Ignore Sentry errors to avoid breaking logging
144
+ }
145
+ }
146
+ }
147
+
148
+ callback();
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Creates a logger instance for a specific service
154
+ * Provides consistent logging format across all packages with level and timestamp
155
+ * @param serviceName The name of the service using the logger
156
+ * @returns A console logger by default, or Winston logger if USE_WINSTON_LOGGER=true
157
+ */
158
+ export function createLogger(serviceName: string): Logger {
159
+ // Use simple console.log logger by default (unbuffered, 12-factor compliant)
160
+ // Set USE_WINSTON_LOGGER=true for Winston features (file rotation, multiple transports)
161
+ if (!USE_WINSTON_LOGGER) {
162
+ return createConsoleLogger(serviceName);
163
+ }
164
+
165
+ const isProduction = process.env.NODE_ENV === "production";
166
+ const level = process.env.LOG_LEVEL || "info";
167
+
168
+ // JSON format for structured logging (better for Loki/Grafana parsing)
169
+ const jsonFormat = winston.format.combine(
170
+ winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }),
171
+ winston.format.json()
172
+ );
173
+
174
+ // Human-readable format for development
175
+ const humanFormat = winston.format.combine(
176
+ ...(isProduction ? [] : [winston.format.colorize()]),
177
+ winston.format.printf(({ timestamp, level, message, service, ...meta }) => {
178
+ let metaStr = "";
179
+ if (Object.keys(meta).length) {
180
+ try {
181
+ metaStr = ` ${JSON.stringify(meta, null, 0)}`;
182
+ } catch (_err) {
183
+ // Handle circular structures with a safer approach
184
+ try {
185
+ const seen = new WeakSet();
186
+ metaStr = ` ${JSON.stringify(meta, (_key, value) => {
187
+ if (typeof value === "object" && value !== null) {
188
+ if (seen.has(value)) {
189
+ return "[Circular Reference]";
190
+ }
191
+ seen.add(value);
192
+
193
+ if (value instanceof Error) {
194
+ return {
195
+ name: value.name,
196
+ message: value.message,
197
+ stack: value.stack?.split("\n")[0], // Only first line of stack
198
+ };
199
+ }
200
+ }
201
+ return value;
202
+ })}`;
203
+ } catch (_err2) {
204
+ // Final fallback if even the circular handler fails
205
+ metaStr = " [Object too complex to serialize]";
206
+ }
207
+ }
208
+ }
209
+ return `[${timestamp}] [${level}] [${service}] ${message}${metaStr}`;
210
+ })
211
+ );
212
+
213
+ const transports: winston.transport[] = [
214
+ new winston.transports.Console({
215
+ format: USE_JSON_FORMAT ? jsonFormat : humanFormat,
216
+ }),
217
+ ];
218
+
219
+ const logger = winston.createLogger({
220
+ level,
221
+ format: winston.format.combine(
222
+ winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
223
+ winston.format.errors({ stack: true }),
224
+ winston.format.splat()
225
+ ),
226
+ defaultMeta: { service: serviceName },
227
+ transports,
228
+ });
229
+
230
+ // Add Sentry transport in production or if SENTRY_DSN is set
231
+ // Deferred to avoid circular dependency with sentry.ts
232
+ // The check is inside setImmediate to ensure SentryTransport class is fully initialized
233
+ setImmediate(() => {
234
+ if (isProduction || process.env.SENTRY_DSN) {
235
+ try {
236
+ const transport = new SentryTransport();
237
+ logger.add(transport);
238
+ } catch {
239
+ // Ignore errors during Sentry transport setup
240
+ }
241
+ }
242
+ });
243
+
244
+ return logger;
245
+ }
246
+
247
+ // Default logger instance for backward compatibility
248
+ export const logger = createLogger("shared");