@johpaz/hive 1.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/CONTRIBUTING.md +44 -0
- package/README.md +310 -0
- package/package.json +96 -0
- package/packages/cli/package.json +28 -0
- package/packages/cli/src/commands/agent-run.ts +168 -0
- package/packages/cli/src/commands/agents.ts +398 -0
- package/packages/cli/src/commands/chat.ts +142 -0
- package/packages/cli/src/commands/config.ts +50 -0
- package/packages/cli/src/commands/cron.ts +161 -0
- package/packages/cli/src/commands/dev.ts +95 -0
- package/packages/cli/src/commands/doctor.ts +133 -0
- package/packages/cli/src/commands/gateway.ts +443 -0
- package/packages/cli/src/commands/logs.ts +57 -0
- package/packages/cli/src/commands/mcp.ts +175 -0
- package/packages/cli/src/commands/message.ts +77 -0
- package/packages/cli/src/commands/onboard.ts +1868 -0
- package/packages/cli/src/commands/security.ts +144 -0
- package/packages/cli/src/commands/service.ts +50 -0
- package/packages/cli/src/commands/sessions.ts +116 -0
- package/packages/cli/src/commands/skills.ts +187 -0
- package/packages/cli/src/commands/update.ts +25 -0
- package/packages/cli/src/index.ts +185 -0
- package/packages/cli/src/utils/token.ts +6 -0
- package/packages/code-bridge/README.md +78 -0
- package/packages/code-bridge/package.json +18 -0
- package/packages/code-bridge/src/index.ts +95 -0
- package/packages/code-bridge/src/process-manager.ts +212 -0
- package/packages/code-bridge/src/schemas.ts +133 -0
- package/packages/core/package.json +46 -0
- package/packages/core/src/agent/agent-loop.ts +369 -0
- package/packages/core/src/agent/compaction.ts +140 -0
- package/packages/core/src/agent/context-compiler.ts +378 -0
- package/packages/core/src/agent/context-guard.ts +91 -0
- package/packages/core/src/agent/context.ts +138 -0
- package/packages/core/src/agent/conversation-store.ts +198 -0
- package/packages/core/src/agent/curator.ts +158 -0
- package/packages/core/src/agent/hooks.ts +166 -0
- package/packages/core/src/agent/index.ts +116 -0
- package/packages/core/src/agent/llm-client.ts +503 -0
- package/packages/core/src/agent/native-tools.ts +505 -0
- package/packages/core/src/agent/prompt-builder.ts +532 -0
- package/packages/core/src/agent/providers/index.ts +167 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/reflector.ts +170 -0
- package/packages/core/src/agent/service.ts +64 -0
- package/packages/core/src/agent/stuck-loop.ts +133 -0
- package/packages/core/src/agent/supervisor.ts +39 -0
- package/packages/core/src/agent/tracer.ts +102 -0
- package/packages/core/src/agent/workspace.ts +110 -0
- package/packages/core/src/canvas/canvas-manager.test.ts +161 -0
- package/packages/core/src/canvas/canvas-manager.ts +319 -0
- package/packages/core/src/canvas/canvas-tools.ts +420 -0
- package/packages/core/src/canvas/emitter.ts +115 -0
- package/packages/core/src/canvas/index.ts +2 -0
- package/packages/core/src/channels/base.ts +138 -0
- package/packages/core/src/channels/discord.ts +260 -0
- package/packages/core/src/channels/index.ts +7 -0
- package/packages/core/src/channels/manager.ts +383 -0
- package/packages/core/src/channels/slack.ts +287 -0
- package/packages/core/src/channels/telegram.ts +502 -0
- package/packages/core/src/channels/webchat.ts +128 -0
- package/packages/core/src/channels/whatsapp.ts +375 -0
- package/packages/core/src/config/index.ts +12 -0
- package/packages/core/src/config/loader.ts +529 -0
- package/packages/core/src/events/event-bus.ts +169 -0
- package/packages/core/src/gateway/index.ts +5 -0
- package/packages/core/src/gateway/initializer.ts +290 -0
- package/packages/core/src/gateway/lane-queue.ts +169 -0
- package/packages/core/src/gateway/resolver.ts +108 -0
- package/packages/core/src/gateway/router.ts +124 -0
- package/packages/core/src/gateway/server.ts +3317 -0
- package/packages/core/src/gateway/session.ts +95 -0
- package/packages/core/src/gateway/slash-commands.ts +192 -0
- package/packages/core/src/heartbeat/index.ts +157 -0
- package/packages/core/src/index.ts +19 -0
- package/packages/core/src/integrations/catalog.ts +286 -0
- package/packages/core/src/integrations/env.ts +64 -0
- package/packages/core/src/integrations/index.ts +2 -0
- package/packages/core/src/memory/index.ts +1 -0
- package/packages/core/src/memory/notes.ts +68 -0
- package/packages/core/src/plugins/api.ts +128 -0
- package/packages/core/src/plugins/index.ts +2 -0
- package/packages/core/src/plugins/loader.ts +365 -0
- package/packages/core/src/resilience/circuit-breaker.ts +225 -0
- package/packages/core/src/security/google-chat.ts +269 -0
- package/packages/core/src/security/index.ts +192 -0
- package/packages/core/src/security/pairing.ts +250 -0
- package/packages/core/src/security/rate-limit.ts +270 -0
- package/packages/core/src/security/signal.ts +321 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/bun-sqlite-store.ts +188 -0
- package/packages/core/src/storage/crypto.ts +101 -0
- package/packages/core/src/storage/db-context.ts +333 -0
- package/packages/core/src/storage/onboarding.ts +1087 -0
- package/packages/core/src/storage/schema.ts +541 -0
- package/packages/core/src/storage/seed.ts +571 -0
- package/packages/core/src/storage/sqlite.ts +387 -0
- package/packages/core/src/storage/usage.ts +212 -0
- package/packages/core/src/tools/bridge-events.ts +74 -0
- package/packages/core/src/tools/browser.ts +275 -0
- package/packages/core/src/tools/codebridge.ts +421 -0
- package/packages/core/src/tools/coordinator-tools.ts +179 -0
- package/packages/core/src/tools/cron.ts +611 -0
- package/packages/core/src/tools/exec.ts +140 -0
- package/packages/core/src/tools/fs.ts +364 -0
- package/packages/core/src/tools/index.ts +12 -0
- package/packages/core/src/tools/memory.ts +176 -0
- package/packages/core/src/tools/notify.ts +113 -0
- package/packages/core/src/tools/project-management.ts +376 -0
- package/packages/core/src/tools/project.ts +375 -0
- package/packages/core/src/tools/read.ts +158 -0
- package/packages/core/src/tools/web.ts +436 -0
- package/packages/core/src/tools/workspace.ts +171 -0
- package/packages/core/src/utils/benchmark.ts +80 -0
- package/packages/core/src/utils/crypto.ts +73 -0
- package/packages/core/src/utils/date.ts +42 -0
- package/packages/core/src/utils/index.ts +4 -0
- package/packages/core/src/utils/logger.ts +388 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/voice/index.ts +583 -0
- package/packages/core/tsconfig.json +9 -0
- package/packages/mcp/package.json +26 -0
- package/packages/mcp/src/config.ts +13 -0
- package/packages/mcp/src/index.ts +1 -0
- package/packages/mcp/src/logger.ts +42 -0
- package/packages/mcp/src/manager.ts +434 -0
- package/packages/mcp/src/transports/index.ts +67 -0
- package/packages/mcp/src/transports/sse.ts +241 -0
- package/packages/mcp/src/transports/websocket.ts +159 -0
- package/packages/skills/package.json +21 -0
- package/packages/skills/src/bundled/agent_management/SKILL.md +24 -0
- package/packages/skills/src/bundled/browser_automation/SKILL.md +30 -0
- package/packages/skills/src/bundled/context_compact/SKILL.md +35 -0
- package/packages/skills/src/bundled/cron_manager/SKILL.md +52 -0
- package/packages/skills/src/bundled/file_manager/SKILL.md +76 -0
- package/packages/skills/src/bundled/http_client/SKILL.md +24 -0
- package/packages/skills/src/bundled/memory/SKILL.md +42 -0
- package/packages/skills/src/bundled/project_management/SKILL.md +26 -0
- package/packages/skills/src/bundled/shell/SKILL.md +43 -0
- package/packages/skills/src/bundled/system_notify/SKILL.md +52 -0
- package/packages/skills/src/bundled/voice/SKILL.md +25 -0
- package/packages/skills/src/bundled/web_search/SKILL.md +29 -0
- package/packages/skills/src/index.ts +1 -0
- package/packages/skills/src/loader.ts +282 -0
- package/packages/tools/package.json +43 -0
- package/packages/tools/src/browser/browser.test.ts +111 -0
- package/packages/tools/src/browser/index.ts +272 -0
- package/packages/tools/src/canvas/index.ts +220 -0
- package/packages/tools/src/cron/cron.test.ts +164 -0
- package/packages/tools/src/cron/index.ts +304 -0
- package/packages/tools/src/filesystem/filesystem.test.ts +240 -0
- package/packages/tools/src/filesystem/index.ts +379 -0
- package/packages/tools/src/git/index.ts +239 -0
- package/packages/tools/src/index.ts +4 -0
- package/packages/tools/src/shell/detect-env.ts +70 -0
- package/packages/tools/tsconfig.json +9 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function generateId(): string {
|
|
4
|
+
return crypto.randomUUID();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function generateShortId(length = 8): string {
|
|
8
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
9
|
+
let result = "";
|
|
10
|
+
const randomBytes = crypto.randomBytes(length);
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < length; i++) {
|
|
13
|
+
result += chars[randomBytes[i]! % chars.length];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function hashString(input: string): string {
|
|
20
|
+
return crypto.createHash("sha256").update(input).digest("hex");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function hashObject(obj: unknown): string {
|
|
24
|
+
return hashString(JSON.stringify(obj));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function hmacSign(
|
|
28
|
+
key: string,
|
|
29
|
+
data: string,
|
|
30
|
+
algorithm: "sha256" | "sha512" = "sha256"
|
|
31
|
+
): Promise<string> {
|
|
32
|
+
return crypto.createHmac(algorithm, key).update(data).digest("hex");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function hmacVerify(
|
|
36
|
+
key: string,
|
|
37
|
+
data: string,
|
|
38
|
+
signature: string,
|
|
39
|
+
algorithm: "sha256" | "sha512" = "sha256"
|
|
40
|
+
): Promise<boolean> {
|
|
41
|
+
const expected = await hmacSign(key, data, algorithm);
|
|
42
|
+
return crypto.timingSafeEqual(
|
|
43
|
+
Buffer.from(expected),
|
|
44
|
+
Buffer.from(signature)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function encrypt(text: string, key: string): string {
|
|
49
|
+
const iv = crypto.randomBytes(16);
|
|
50
|
+
const derivedKey = crypto.scryptSync(key, "salt", 32);
|
|
51
|
+
const cipher = crypto.createCipheriv("aes-256-cbc", derivedKey, iv);
|
|
52
|
+
|
|
53
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
54
|
+
encrypted += cipher.final("hex");
|
|
55
|
+
|
|
56
|
+
return iv.toString("hex") + ":" + encrypted;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function decrypt(encryptedData: string, key: string): string {
|
|
60
|
+
const [ivHex, encrypted] = encryptedData.split(":");
|
|
61
|
+
if (!ivHex || !encrypted) {
|
|
62
|
+
throw new Error("Invalid encrypted data format");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
66
|
+
const derivedKey = crypto.scryptSync(key, "salt", 32);
|
|
67
|
+
const decipher = crypto.createDecipheriv("aes-256-cbc", derivedKey, iv);
|
|
68
|
+
|
|
69
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
70
|
+
decrypted += decipher.final("utf8");
|
|
71
|
+
|
|
72
|
+
return decrypted;
|
|
73
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for user-specific date and time formatting.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function getUserDate(timezone: string = "UTC", date: Date = new Date()): string {
|
|
6
|
+
try {
|
|
7
|
+
const formatter = new Intl.DateTimeFormat("en-GB", {
|
|
8
|
+
timeZone: timezone,
|
|
9
|
+
year: "numeric",
|
|
10
|
+
month: "2-digit",
|
|
11
|
+
day: "2-digit",
|
|
12
|
+
});
|
|
13
|
+
const parts = formatter.formatToParts(date);
|
|
14
|
+
const year = parts.find(p => p.type === "year")?.value;
|
|
15
|
+
const month = parts.find(p => p.type === "month")?.value;
|
|
16
|
+
const day = parts.find(p => p.type === "day")?.value;
|
|
17
|
+
return `${year}-${month}-${day}`;
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// Fallback to UTC if timezone is invalid
|
|
20
|
+
return date.toISOString().split("T")[0];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getUserTime(timezone: string = "UTC", date: Date = new Date()): string {
|
|
25
|
+
try {
|
|
26
|
+
const formatter = new Intl.DateTimeFormat("en-GB", {
|
|
27
|
+
timeZone: timezone,
|
|
28
|
+
hour: "2-digit",
|
|
29
|
+
minute: "2-digit",
|
|
30
|
+
second: "2-digit",
|
|
31
|
+
hour12: false,
|
|
32
|
+
});
|
|
33
|
+
const parts = formatter.formatToParts(date);
|
|
34
|
+
const hour = parts.find(p => p.type === "hour")?.value;
|
|
35
|
+
const minute = parts.find(p => p.type === "minute")?.value;
|
|
36
|
+
const second = parts.find(p => p.type === "second")?.value;
|
|
37
|
+
return `${hour}:${minute}:${second}`;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// Fallback if formatting fails
|
|
40
|
+
return date.toISOString().split("T")[1].split(".")[0];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { mkdirSync, unlinkSync, renameSync, existsSync } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getHiveDir } from "../config/loader.ts";
|
|
4
|
+
|
|
5
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
6
|
+
|
|
7
|
+
export interface LogEntry {
|
|
8
|
+
timestamp: string;
|
|
9
|
+
level: LogLevel;
|
|
10
|
+
source: string;
|
|
11
|
+
message: string;
|
|
12
|
+
meta?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type LogEntryListener = (entry: LogEntry) => void;
|
|
16
|
+
|
|
17
|
+
const _logListeners: Set<LogEntryListener> = new Set();
|
|
18
|
+
|
|
19
|
+
/** Subscribe to real-time log entries */
|
|
20
|
+
export function onLogEntry(cb: LogEntryListener): void {
|
|
21
|
+
_logListeners.add(cb);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Unsubscribe from real-time log entries */
|
|
25
|
+
export function removeLogListener(cb: LogEntryListener): void {
|
|
26
|
+
_logListeners.delete(cb);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function emitLogEntry(entry: LogEntry): void {
|
|
30
|
+
for (const cb of _logListeners) {
|
|
31
|
+
try { cb(entry); } catch { /* listener error should not crash logger */ }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LoggerConfig {
|
|
36
|
+
level: LogLevel;
|
|
37
|
+
dir: string;
|
|
38
|
+
maxSizeMB: number;
|
|
39
|
+
maxFiles: number;
|
|
40
|
+
redactSensitive: boolean;
|
|
41
|
+
console: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface LogMeta extends Record<string, unknown> {
|
|
45
|
+
correlationId?: string;
|
|
46
|
+
sessionId?: string;
|
|
47
|
+
userId?: string;
|
|
48
|
+
agentId?: string;
|
|
49
|
+
channel?: string;
|
|
50
|
+
toolName?: string;
|
|
51
|
+
duration?: number;
|
|
52
|
+
error?: string;
|
|
53
|
+
stack?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
57
|
+
debug: 0,
|
|
58
|
+
info: 1,
|
|
59
|
+
warn: 2,
|
|
60
|
+
error: 3,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const SENSITIVE_PATTERNS = [
|
|
64
|
+
/api[_-]?key/i,
|
|
65
|
+
/token/i,
|
|
66
|
+
/secret/i,
|
|
67
|
+
/password/i,
|
|
68
|
+
/credential/i,
|
|
69
|
+
/auth/i,
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const COLORS = {
|
|
73
|
+
debug: "\x1b[36m",
|
|
74
|
+
info: "\x1b[32m",
|
|
75
|
+
warn: "\x1b[33m",
|
|
76
|
+
error: "\x1b[31m",
|
|
77
|
+
reset: "\x1b[0m",
|
|
78
|
+
dim: "\x1b[2m",
|
|
79
|
+
bright: "\x1b[1m",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function expandPath(p: string): string {
|
|
83
|
+
if (p.startsWith("~")) {
|
|
84
|
+
return path.join(process.env.HOME || "", p.slice(1));
|
|
85
|
+
}
|
|
86
|
+
return p;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function redact(obj: unknown, seen: WeakSet<object> = new WeakSet()): unknown {
|
|
90
|
+
if (obj === null || typeof obj !== "object") {
|
|
91
|
+
return obj;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (seen.has(obj as object)) {
|
|
95
|
+
return "[Circular]";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
seen.add(obj as object);
|
|
99
|
+
|
|
100
|
+
if (Array.isArray(obj)) {
|
|
101
|
+
return obj.map((item) => redact(item, seen));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result: Record<string, unknown> = {};
|
|
105
|
+
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
106
|
+
const isSensitive = SENSITIVE_PATTERNS.some((p) => p.test(key));
|
|
107
|
+
if (isSensitive) {
|
|
108
|
+
result[key] = "[REDACTED]";
|
|
109
|
+
} else if (typeof value === "object" && value !== null) {
|
|
110
|
+
result[key] = redact(value, seen);
|
|
111
|
+
} else {
|
|
112
|
+
result[key] = value;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatTimestamp(): string {
|
|
119
|
+
return new Date().toISOString();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatMessage(level: LogLevel, message: string, meta?: unknown, correlationId?: string): string {
|
|
123
|
+
const timestamp = formatTimestamp();
|
|
124
|
+
const corrStr = correlationId ? ` [${correlationId.slice(0, 8)}]` : "";
|
|
125
|
+
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
|
|
126
|
+
return `[${timestamp}]${corrStr} [${level.toUpperCase()}] ${message}${metaStr}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export class Logger {
|
|
130
|
+
private config: LoggerConfig;
|
|
131
|
+
private logFile: string | null = null;
|
|
132
|
+
private currentSize = 0;
|
|
133
|
+
private correlationContext: LogMeta = {};
|
|
134
|
+
|
|
135
|
+
constructor(config: Partial<LoggerConfig> = {}) {
|
|
136
|
+
this.config = {
|
|
137
|
+
level: config.level ?? "info",
|
|
138
|
+
dir: config.dir ?? path.join(getHiveDir(), "logs"),
|
|
139
|
+
maxSizeMB: config.maxSizeMB ?? 10,
|
|
140
|
+
maxFiles: config.maxFiles ?? 5,
|
|
141
|
+
redactSensitive: config.redactSensitive ?? true,
|
|
142
|
+
console: config.console ?? true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
setCorrelationContext(context: Partial<LogMeta>): void {
|
|
147
|
+
this.correlationContext = { ...this.correlationContext, ...context };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
clearCorrelationContext(): void {
|
|
151
|
+
this.correlationContext = {};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getCorrelationId(): string | undefined {
|
|
155
|
+
return this.correlationContext.correlationId;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
withCorrelationId(id: string): this {
|
|
159
|
+
this.correlationContext.correlationId = id;
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private initLogFile(): void {
|
|
164
|
+
const logDir = expandPath(this.config.dir);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
if (!existsSync(logDir)) {
|
|
168
|
+
mkdirSync(logDir, { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.logFile = path.join(logDir, `hive-${new Date().toISOString().split("T")[0]}.log`);
|
|
172
|
+
|
|
173
|
+
const file = Bun.file(this.logFile);
|
|
174
|
+
this.currentSize = file.size ?? 0;
|
|
175
|
+
} catch {
|
|
176
|
+
this.logFile = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private shouldLog(level: LogLevel): boolean {
|
|
181
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[this.config.level];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private writeToConsole(level: LogLevel, message: string, meta?: unknown): void {
|
|
185
|
+
if (!this.config.console) return;
|
|
186
|
+
|
|
187
|
+
const color = COLORS[level];
|
|
188
|
+
const mergedMeta = this.mergeMeta(meta);
|
|
189
|
+
const displayMeta = this.config.redactSensitive && mergedMeta ? redact(mergedMeta) : mergedMeta;
|
|
190
|
+
const metaStr = displayMeta && Object.keys(displayMeta as object).length > 0
|
|
191
|
+
? ` ${JSON.stringify(displayMeta)}`
|
|
192
|
+
: "";
|
|
193
|
+
|
|
194
|
+
const prefix = `${COLORS.dim}${formatTimestamp()}${COLORS.reset}`;
|
|
195
|
+
const corrStr = this.correlationContext.correlationId
|
|
196
|
+
? ` ${COLORS.dim}[${this.correlationContext.correlationId.slice(0, 8)}]${COLORS.reset}`
|
|
197
|
+
: "";
|
|
198
|
+
const levelStr = `${color}${COLORS.bright}[${level.toUpperCase().padEnd(5)}]${COLORS.reset}`;
|
|
199
|
+
|
|
200
|
+
console.log(`${prefix}${corrStr} ${levelStr} ${message}${metaStr}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private mergeMeta(meta?: unknown): LogMeta | undefined {
|
|
204
|
+
if (!meta && Object.keys(this.correlationContext).length === 0) return undefined;
|
|
205
|
+
|
|
206
|
+
const contextWithoutCorrId = { ...this.correlationContext };
|
|
207
|
+
delete contextWithoutCorrId.correlationId;
|
|
208
|
+
|
|
209
|
+
if (!meta) return contextWithoutCorrId;
|
|
210
|
+
if (typeof meta !== "object") return meta as LogMeta;
|
|
211
|
+
|
|
212
|
+
return { ...contextWithoutCorrId, ...(meta as LogMeta) };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private writeToFile(message: string): void {
|
|
216
|
+
if (!this.logFile) {
|
|
217
|
+
this.initLogFile();
|
|
218
|
+
}
|
|
219
|
+
if (!this.logFile) return;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const line = message + "\n";
|
|
223
|
+
const bytes = Buffer.byteLength(line);
|
|
224
|
+
|
|
225
|
+
if (this.currentSize + bytes > this.config.maxSizeMB * 1024 * 1024) {
|
|
226
|
+
this.rotateLogs();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Use sync append for logging reliability
|
|
230
|
+
const encoder = new TextEncoder();
|
|
231
|
+
const data = encoder.encode(line);
|
|
232
|
+
Bun.write(this.logFile, data).catch(() => { });
|
|
233
|
+
this.currentSize += bytes;
|
|
234
|
+
} catch {
|
|
235
|
+
// Silently fail if we can't write to log file
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private rotateLogs(): void {
|
|
240
|
+
if (!this.logFile) return;
|
|
241
|
+
|
|
242
|
+
const logDir = path.dirname(this.logFile);
|
|
243
|
+
const baseName = path.basename(this.logFile, ".log");
|
|
244
|
+
|
|
245
|
+
for (let i = this.config.maxFiles - 1; i >= 1; i--) {
|
|
246
|
+
const oldFile = path.join(logDir, `${baseName}.${i}.log`);
|
|
247
|
+
const newFile = path.join(logDir, `${baseName}.${i + 1}.log`);
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
if (existsSync(oldFile)) {
|
|
251
|
+
if (i === this.config.maxFiles - 1) {
|
|
252
|
+
unlinkSync(oldFile);
|
|
253
|
+
} else {
|
|
254
|
+
renameSync(oldFile, newFile);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
// Continue rotation even if one file fails
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
renameSync(this.logFile, path.join(logDir, `${baseName}.1.log`));
|
|
264
|
+
this.currentSize = 0;
|
|
265
|
+
} catch {
|
|
266
|
+
// Continue even if rotation fails
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
debug(message: string, meta?: unknown): void {
|
|
271
|
+
if (!this.shouldLog("debug")) return;
|
|
272
|
+
const mergedMeta = this.mergeMeta(meta);
|
|
273
|
+
const formatted = formatMessage("debug", message, mergedMeta, this.correlationContext.correlationId);
|
|
274
|
+
this.writeToConsole("debug", message, meta);
|
|
275
|
+
this.writeToFile(formatted);
|
|
276
|
+
emitLogEntry({ timestamp: formatTimestamp(), level: "debug", source: "core", message, meta: mergedMeta as Record<string, unknown> | undefined });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
info(message: string, meta?: unknown): void {
|
|
280
|
+
if (!this.shouldLog("info")) return;
|
|
281
|
+
const mergedMeta = this.mergeMeta(meta);
|
|
282
|
+
const formatted = formatMessage("info", message, mergedMeta, this.correlationContext.correlationId);
|
|
283
|
+
this.writeToConsole("info", message, meta);
|
|
284
|
+
this.writeToFile(formatted);
|
|
285
|
+
emitLogEntry({ timestamp: formatTimestamp(), level: "info", source: "core", message, meta: mergedMeta as Record<string, unknown> | undefined });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
warn(message: string, meta?: unknown): void {
|
|
289
|
+
if (!this.shouldLog("warn")) return;
|
|
290
|
+
const mergedMeta = this.mergeMeta(meta);
|
|
291
|
+
const formatted = formatMessage("warn", message, mergedMeta, this.correlationContext.correlationId);
|
|
292
|
+
this.writeToConsole("warn", message, meta);
|
|
293
|
+
this.writeToFile(formatted);
|
|
294
|
+
emitLogEntry({ timestamp: formatTimestamp(), level: "warn", source: "core", message, meta: mergedMeta as Record<string, unknown> | undefined });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
error(message: string, meta?: unknown): void {
|
|
298
|
+
if (!this.shouldLog("error")) return;
|
|
299
|
+
const mergedMeta = this.mergeMeta(meta);
|
|
300
|
+
const formatted = formatMessage("error", message, mergedMeta, this.correlationContext.correlationId);
|
|
301
|
+
this.writeToConsole("error", message, meta);
|
|
302
|
+
this.writeToFile(formatted);
|
|
303
|
+
emitLogEntry({ timestamp: formatTimestamp(), level: "error", source: "core", message, meta: mergedMeta as Record<string, unknown> | undefined });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
child(context: string): ChildLogger {
|
|
307
|
+
return new ChildLogger(this, context, this.correlationContext);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
setLevel(level: LogLevel): void {
|
|
311
|
+
this.config.level = level;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export class ChildLogger {
|
|
316
|
+
constructor(
|
|
317
|
+
private parent: Logger,
|
|
318
|
+
private context: string,
|
|
319
|
+
private correlationContext: LogMeta = {}
|
|
320
|
+
) { }
|
|
321
|
+
|
|
322
|
+
private prefix(message: string): string {
|
|
323
|
+
return `[${this.context}] ${message}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
withCorrelationId(id: string): this {
|
|
327
|
+
this.correlationContext.correlationId = id;
|
|
328
|
+
return this;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
setContext(context: Partial<LogMeta>): void {
|
|
332
|
+
this.correlationContext = { ...this.correlationContext, ...context };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
debug(message: string, meta?: unknown): void {
|
|
336
|
+
this.parent.debug(this.prefix(message), this.mergeMeta(meta));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
info(message: string, meta?: unknown): void {
|
|
340
|
+
this.parent.info(this.prefix(message), this.mergeMeta(meta));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
warn(message: string, meta?: unknown): void {
|
|
344
|
+
this.parent.warn(this.prefix(message), this.mergeMeta(meta));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
error(message: string, meta?: unknown): void {
|
|
348
|
+
this.parent.error(this.prefix(message), this.mergeMeta(meta));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
child(subContext: string): ChildLogger {
|
|
352
|
+
return new ChildLogger(
|
|
353
|
+
this.parent,
|
|
354
|
+
`${this.context}:${subContext}`,
|
|
355
|
+
this.correlationContext
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private mergeMeta(meta?: unknown): LogMeta | undefined {
|
|
360
|
+
if (!meta && Object.keys(this.correlationContext).length === 0) return undefined;
|
|
361
|
+
if (!meta) return { ...this.correlationContext };
|
|
362
|
+
if (typeof meta !== "object") return meta as LogMeta;
|
|
363
|
+
return { ...this.correlationContext, ...(meta as LogMeta) };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let _logger: Logger | null = null;
|
|
368
|
+
|
|
369
|
+
export function getLogger(): Logger {
|
|
370
|
+
if (!_logger) {
|
|
371
|
+
_logger = new Logger();
|
|
372
|
+
}
|
|
373
|
+
return _logger;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export const logger = {
|
|
377
|
+
child: (opts: any) => getLogger().child(opts),
|
|
378
|
+
debug: (msg: string, meta?: unknown) => getLogger().debug(msg, meta),
|
|
379
|
+
info: (msg: string, meta?: unknown) => getLogger().info(msg, meta),
|
|
380
|
+
warn: (msg: string, meta?: unknown) => getLogger().warn(msg, meta),
|
|
381
|
+
error: (msg: string, meta?: unknown) => getLogger().error(msg, meta),
|
|
382
|
+
setCorrelationContext: (ctx: any) => getLogger().setCorrelationContext(ctx),
|
|
383
|
+
clearCorrelationContext: () => getLogger().clearCorrelationContext(),
|
|
384
|
+
getCorrelationId: () => getLogger().getCorrelationId(),
|
|
385
|
+
withCorrelationId: (id: string) => getLogger().withCorrelationId(id),
|
|
386
|
+
setLevel: (level: any) => getLogger().setLevel(level),
|
|
387
|
+
setHandler: (handler: any) => { /* no-op for compatibility */ },
|
|
388
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface RetryOptions {
|
|
2
|
+
maxAttempts: number;
|
|
3
|
+
initialDelayMs: number;
|
|
4
|
+
backoffMultiplier: number;
|
|
5
|
+
maxDelayMs: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const DEFAULT_OPTIONS: RetryOptions = {
|
|
9
|
+
maxAttempts: 3,
|
|
10
|
+
initialDelayMs: 1000,
|
|
11
|
+
backoffMultiplier: 2,
|
|
12
|
+
maxDelayMs: 30000,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function sleep(ms: number): Promise<void> {
|
|
16
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function retry<T>(
|
|
20
|
+
fn: () => Promise<T>,
|
|
21
|
+
options: Partial<RetryOptions> = {}
|
|
22
|
+
): Promise<T> {
|
|
23
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
24
|
+
let lastError: Error | undefined;
|
|
25
|
+
let delay = opts.initialDelayMs;
|
|
26
|
+
|
|
27
|
+
for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
|
|
28
|
+
try {
|
|
29
|
+
return await fn();
|
|
30
|
+
} catch (error) {
|
|
31
|
+
lastError = error as Error;
|
|
32
|
+
|
|
33
|
+
if (attempt === opts.maxAttempts) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await sleep(delay);
|
|
38
|
+
delay = Math.min(delay * opts.backoffMultiplier, opts.maxDelayMs);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
throw lastError ?? new Error("Retry failed");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function retryWithBackoff<T>(
|
|
46
|
+
fn: () => Promise<T>,
|
|
47
|
+
shouldRetry: (error: Error) => boolean,
|
|
48
|
+
options: Partial<RetryOptions> = {}
|
|
49
|
+
): Promise<T> {
|
|
50
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
51
|
+
let lastError: Error | undefined;
|
|
52
|
+
let delay = opts.initialDelayMs;
|
|
53
|
+
|
|
54
|
+
for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
|
|
55
|
+
try {
|
|
56
|
+
return await fn();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
lastError = error as Error;
|
|
59
|
+
|
|
60
|
+
if (!shouldRetry(lastError) || attempt === opts.maxAttempts) {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await sleep(delay);
|
|
65
|
+
delay = Math.min(delay * opts.backoffMultiplier, opts.maxDelayMs);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw lastError ?? new Error("Retry failed");
|
|
70
|
+
}
|