@johpaz/hive-core 0.1.1
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/package.json +43 -0
- package/src/agent/compaction.ts +161 -0
- package/src/agent/context-guard.ts +91 -0
- package/src/agent/context.ts +148 -0
- package/src/agent/ethics.ts +102 -0
- package/src/agent/hooks.ts +166 -0
- package/src/agent/index.ts +67 -0
- package/src/agent/providers/index.ts +278 -0
- package/src/agent/providers.ts +1 -0
- package/src/agent/soul.ts +89 -0
- package/src/agent/stuck-loop.ts +133 -0
- package/src/agent/user.ts +86 -0
- package/src/channels/base.ts +91 -0
- package/src/channels/discord.ts +185 -0
- package/src/channels/index.ts +7 -0
- package/src/channels/manager.ts +204 -0
- package/src/channels/slack.ts +209 -0
- package/src/channels/telegram.ts +177 -0
- package/src/channels/webchat.ts +83 -0
- package/src/channels/whatsapp.ts +305 -0
- package/src/config/index.ts +1 -0
- package/src/config/loader.ts +508 -0
- package/src/gateway/index.ts +5 -0
- package/src/gateway/lane-queue.ts +169 -0
- package/src/gateway/router.ts +124 -0
- package/src/gateway/server.ts +347 -0
- package/src/gateway/session.ts +131 -0
- package/src/gateway/slash-commands.ts +176 -0
- package/src/heartbeat/index.ts +157 -0
- package/src/index.ts +21 -0
- package/src/memory/index.ts +1 -0
- package/src/memory/notes.ts +170 -0
- package/src/multi-agent/bindings.ts +171 -0
- package/src/multi-agent/index.ts +4 -0
- package/src/multi-agent/manager.ts +182 -0
- package/src/multi-agent/sandbox.ts +130 -0
- package/src/multi-agent/subagents.ts +302 -0
- package/src/security/index.ts +187 -0
- package/src/tools/cron.ts +156 -0
- package/src/tools/exec.ts +105 -0
- package/src/tools/index.ts +6 -0
- package/src/tools/memory.ts +176 -0
- package/src/tools/notify.ts +53 -0
- package/src/tools/read.ts +154 -0
- package/src/tools/registry.ts +115 -0
- package/src/tools/web.ts +186 -0
- package/src/utils/crypto.ts +73 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/logger.ts +254 -0
- package/src/utils/retry.ts +70 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
5
|
+
|
|
6
|
+
export interface LoggerConfig {
|
|
7
|
+
level: LogLevel;
|
|
8
|
+
dir: string;
|
|
9
|
+
maxSizeMB: number;
|
|
10
|
+
maxFiles: number;
|
|
11
|
+
redactSensitive: boolean;
|
|
12
|
+
console: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
16
|
+
debug: 0,
|
|
17
|
+
info: 1,
|
|
18
|
+
warn: 2,
|
|
19
|
+
error: 3,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const SENSITIVE_PATTERNS = [
|
|
23
|
+
/api[_-]?key/i,
|
|
24
|
+
/token/i,
|
|
25
|
+
/secret/i,
|
|
26
|
+
/password/i,
|
|
27
|
+
/credential/i,
|
|
28
|
+
/auth/i,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const COLORS = {
|
|
32
|
+
debug: "\x1b[36m",
|
|
33
|
+
info: "\x1b[32m",
|
|
34
|
+
warn: "\x1b[33m",
|
|
35
|
+
error: "\x1b[31m",
|
|
36
|
+
reset: "\x1b[0m",
|
|
37
|
+
dim: "\x1b[2m",
|
|
38
|
+
bright: "\x1b[1m",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function expandPath(p: string): string {
|
|
42
|
+
if (p.startsWith("~")) {
|
|
43
|
+
return path.join(process.env.HOME || "", p.slice(1));
|
|
44
|
+
}
|
|
45
|
+
return p;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function redact(obj: unknown, seen: WeakSet<object> = new WeakSet()): unknown {
|
|
49
|
+
if (obj === null || typeof obj !== "object") {
|
|
50
|
+
return obj;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (seen.has(obj as object)) {
|
|
54
|
+
return "[Circular]";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
seen.add(obj as object);
|
|
58
|
+
|
|
59
|
+
if (Array.isArray(obj)) {
|
|
60
|
+
return obj.map((item) => redact(item, seen));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result: Record<string, unknown> = {};
|
|
64
|
+
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
65
|
+
const isSensitive = SENSITIVE_PATTERNS.some((p) => p.test(key));
|
|
66
|
+
if (isSensitive) {
|
|
67
|
+
result[key] = "[REDACTED]";
|
|
68
|
+
} else if (typeof value === "object" && value !== null) {
|
|
69
|
+
result[key] = redact(value, seen);
|
|
70
|
+
} else {
|
|
71
|
+
result[key] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatTimestamp(): string {
|
|
78
|
+
return new Date().toISOString();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatMessage(level: LogLevel, message: string, meta?: unknown): string {
|
|
82
|
+
const timestamp = formatTimestamp();
|
|
83
|
+
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
|
|
84
|
+
return `[${timestamp}] [${level.toUpperCase()}] ${message}${metaStr}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class Logger {
|
|
88
|
+
private config: LoggerConfig;
|
|
89
|
+
private logFile: string | null = null;
|
|
90
|
+
private currentSize = 0;
|
|
91
|
+
|
|
92
|
+
constructor(config: Partial<LoggerConfig> = {}) {
|
|
93
|
+
this.config = {
|
|
94
|
+
level: config.level ?? "info",
|
|
95
|
+
dir: config.dir ?? "~/.hive/logs",
|
|
96
|
+
maxSizeMB: config.maxSizeMB ?? 10,
|
|
97
|
+
maxFiles: config.maxFiles ?? 5,
|
|
98
|
+
redactSensitive: config.redactSensitive ?? true,
|
|
99
|
+
console: config.console ?? true,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
this.initLogFile();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private initLogFile(): void {
|
|
106
|
+
const logDir = expandPath(this.config.dir);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
if (!fs.existsSync(logDir)) {
|
|
110
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.logFile = path.join(logDir, `hive-${new Date().toISOString().split("T")[0]}.log`);
|
|
114
|
+
|
|
115
|
+
if (fs.existsSync(this.logFile)) {
|
|
116
|
+
const stats = fs.statSync(this.logFile);
|
|
117
|
+
this.currentSize = stats.size;
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
this.logFile = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private shouldLog(level: LogLevel): boolean {
|
|
125
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[this.config.level];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private writeToConsole(level: LogLevel, message: string, meta?: unknown): void {
|
|
129
|
+
if (!this.config.console) return;
|
|
130
|
+
|
|
131
|
+
const color = COLORS[level];
|
|
132
|
+
const displayMeta = this.config.redactSensitive && meta ? redact(meta) : meta;
|
|
133
|
+
const metaStr = displayMeta ? ` ${JSON.stringify(displayMeta)}` : "";
|
|
134
|
+
|
|
135
|
+
const prefix = `${COLORS.dim}${formatTimestamp()}${COLORS.reset}`;
|
|
136
|
+
const levelStr = `${color}${COLORS.bright}[${level.toUpperCase().padEnd(5)}]${COLORS.reset}`;
|
|
137
|
+
|
|
138
|
+
console.log(`${prefix} ${levelStr} ${message}${metaStr}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private writeToFile(message: string): void {
|
|
142
|
+
if (!this.logFile) return;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const line = message + "\n";
|
|
146
|
+
const bytes = Buffer.byteLength(line);
|
|
147
|
+
|
|
148
|
+
if (this.currentSize + bytes > this.config.maxSizeMB * 1024 * 1024) {
|
|
149
|
+
this.rotateLogs();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fs.appendFileSync(this.logFile, line);
|
|
153
|
+
this.currentSize += bytes;
|
|
154
|
+
} catch {
|
|
155
|
+
// Silently fail if we can't write to log file
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private rotateLogs(): void {
|
|
160
|
+
if (!this.logFile) return;
|
|
161
|
+
|
|
162
|
+
const logDir = path.dirname(this.logFile);
|
|
163
|
+
const baseName = path.basename(this.logFile, ".log");
|
|
164
|
+
|
|
165
|
+
for (let i = this.config.maxFiles - 1; i >= 1; i--) {
|
|
166
|
+
const oldFile = path.join(logDir, `${baseName}.${i}.log`);
|
|
167
|
+
const newFile = path.join(logDir, `${baseName}.${i + 1}.log`);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
if (fs.existsSync(oldFile)) {
|
|
171
|
+
if (i === this.config.maxFiles - 1) {
|
|
172
|
+
fs.unlinkSync(oldFile);
|
|
173
|
+
} else {
|
|
174
|
+
fs.renameSync(oldFile, newFile);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
// Continue rotation even if one file fails
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
fs.renameSync(this.logFile, path.join(logDir, `${baseName}.1.log`));
|
|
184
|
+
this.currentSize = 0;
|
|
185
|
+
} catch {
|
|
186
|
+
// Continue even if rotation fails
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
debug(message: string, meta?: unknown): void {
|
|
191
|
+
if (!this.shouldLog("debug")) return;
|
|
192
|
+
const formatted = formatMessage("debug", message, meta);
|
|
193
|
+
this.writeToConsole("debug", message, meta);
|
|
194
|
+
this.writeToFile(formatted);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
info(message: string, meta?: unknown): void {
|
|
198
|
+
if (!this.shouldLog("info")) return;
|
|
199
|
+
const formatted = formatMessage("info", message, meta);
|
|
200
|
+
this.writeToConsole("info", message, meta);
|
|
201
|
+
this.writeToFile(formatted);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
warn(message: string, meta?: unknown): void {
|
|
205
|
+
if (!this.shouldLog("warn")) return;
|
|
206
|
+
const formatted = formatMessage("warn", message, meta);
|
|
207
|
+
this.writeToConsole("warn", message, meta);
|
|
208
|
+
this.writeToFile(formatted);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
error(message: string, meta?: unknown): void {
|
|
212
|
+
if (!this.shouldLog("error")) return;
|
|
213
|
+
const formatted = formatMessage("error", message, meta);
|
|
214
|
+
this.writeToConsole("error", message, meta);
|
|
215
|
+
this.writeToFile(formatted);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
child(context: string): ChildLogger {
|
|
219
|
+
return new ChildLogger(this, context);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
setLevel(level: LogLevel): void {
|
|
223
|
+
this.config.level = level;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export class ChildLogger {
|
|
228
|
+
constructor(
|
|
229
|
+
private parent: Logger,
|
|
230
|
+
private context: string
|
|
231
|
+
) {}
|
|
232
|
+
|
|
233
|
+
private prefix(message: string): string {
|
|
234
|
+
return `[${this.context}] ${message}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
debug(message: string, meta?: unknown): void {
|
|
238
|
+
this.parent.debug(this.prefix(message), meta);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
info(message: string, meta?: unknown): void {
|
|
242
|
+
this.parent.info(this.prefix(message), meta);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
warn(message: string, meta?: unknown): void {
|
|
246
|
+
this.parent.warn(this.prefix(message), meta);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
error(message: string, meta?: unknown): void {
|
|
250
|
+
this.parent.error(this.prefix(message), meta);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export const logger = new Logger();
|
|
@@ -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
|
+
}
|