claude-flow 1.0.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/LICENSE +21 -0
- package/README.md +612 -0
- package/bin/claude-flow +0 -0
- package/bin/claude-flow-simple +0 -0
- package/bin/claude-flow-typecheck +0 -0
- package/deno.json +84 -0
- package/package.json +45 -0
- package/scripts/check-links.ts +274 -0
- package/scripts/check-performance-regression.ts +168 -0
- package/scripts/claude-sparc.sh +562 -0
- package/scripts/coverage-report.ts +692 -0
- package/scripts/demo-task-system.ts +224 -0
- package/scripts/install.js +72 -0
- package/scripts/test-batch-tasks.ts +29 -0
- package/scripts/test-coordination-features.ts +238 -0
- package/scripts/test-mcp.ts +251 -0
- package/scripts/test-runner.ts +571 -0
- package/scripts/validate-examples.ts +288 -0
- package/src/cli/cli-core.ts +273 -0
- package/src/cli/commands/agent.ts +83 -0
- package/src/cli/commands/config.ts +442 -0
- package/src/cli/commands/help.ts +765 -0
- package/src/cli/commands/index.ts +963 -0
- package/src/cli/commands/mcp.ts +191 -0
- package/src/cli/commands/memory.ts +74 -0
- package/src/cli/commands/monitor.ts +403 -0
- package/src/cli/commands/session.ts +595 -0
- package/src/cli/commands/start.ts +156 -0
- package/src/cli/commands/status.ts +345 -0
- package/src/cli/commands/task.ts +79 -0
- package/src/cli/commands/workflow.ts +763 -0
- package/src/cli/completion.ts +553 -0
- package/src/cli/formatter.ts +310 -0
- package/src/cli/index.ts +211 -0
- package/src/cli/main.ts +23 -0
- package/src/cli/repl.ts +1050 -0
- package/src/cli/simple-cli.js +211 -0
- package/src/cli/simple-cli.ts +211 -0
- package/src/coordination/README.md +400 -0
- package/src/coordination/advanced-scheduler.ts +487 -0
- package/src/coordination/circuit-breaker.ts +366 -0
- package/src/coordination/conflict-resolution.ts +490 -0
- package/src/coordination/dependency-graph.ts +475 -0
- package/src/coordination/index.ts +63 -0
- package/src/coordination/manager.ts +460 -0
- package/src/coordination/messaging.ts +290 -0
- package/src/coordination/metrics.ts +585 -0
- package/src/coordination/resources.ts +322 -0
- package/src/coordination/scheduler.ts +390 -0
- package/src/coordination/work-stealing.ts +224 -0
- package/src/core/config.ts +627 -0
- package/src/core/event-bus.ts +186 -0
- package/src/core/json-persistence.ts +183 -0
- package/src/core/logger.ts +262 -0
- package/src/core/orchestrator-fixed.ts +312 -0
- package/src/core/orchestrator.ts +1234 -0
- package/src/core/persistence.ts +276 -0
- package/src/mcp/auth.ts +438 -0
- package/src/mcp/claude-flow-tools.ts +1280 -0
- package/src/mcp/load-balancer.ts +510 -0
- package/src/mcp/router.ts +240 -0
- package/src/mcp/server.ts +548 -0
- package/src/mcp/session-manager.ts +418 -0
- package/src/mcp/tools.ts +180 -0
- package/src/mcp/transports/base.ts +21 -0
- package/src/mcp/transports/http.ts +457 -0
- package/src/mcp/transports/stdio.ts +254 -0
- package/src/memory/backends/base.ts +22 -0
- package/src/memory/backends/markdown.ts +283 -0
- package/src/memory/backends/sqlite.ts +329 -0
- package/src/memory/cache.ts +238 -0
- package/src/memory/indexer.ts +238 -0
- package/src/memory/manager.ts +572 -0
- package/src/terminal/adapters/base.ts +29 -0
- package/src/terminal/adapters/native.ts +504 -0
- package/src/terminal/adapters/vscode.ts +340 -0
- package/src/terminal/manager.ts +308 -0
- package/src/terminal/pool.ts +271 -0
- package/src/terminal/session.ts +250 -0
- package/src/terminal/vscode-bridge.ts +242 -0
- package/src/utils/errors.ts +231 -0
- package/src/utils/helpers.ts +476 -0
- package/src/utils/types.ts +493 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event bus implementation for Claude-Flow
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SystemEvents, EventMap } from '../utils/types.ts';
|
|
6
|
+
import { TypedEventEmitter } from '../utils/helpers.ts';
|
|
7
|
+
|
|
8
|
+
export interface IEventBus {
|
|
9
|
+
emit(event: string, data?: unknown): void;
|
|
10
|
+
on(event: string, handler: (data: unknown) => void): void;
|
|
11
|
+
off(event: string, handler: (data: unknown) => void): void;
|
|
12
|
+
once(event: string, handler: (data: unknown) => void): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Internal typed event bus
|
|
17
|
+
*/
|
|
18
|
+
class TypedEventBus extends TypedEventEmitter<EventMap> {
|
|
19
|
+
private eventCounts = new Map<keyof EventMap, number>();
|
|
20
|
+
private lastEventTimes = new Map<keyof EventMap, number>();
|
|
21
|
+
private debug: boolean;
|
|
22
|
+
|
|
23
|
+
constructor(debug = false) {
|
|
24
|
+
super();
|
|
25
|
+
this.debug = debug;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Emits an event with logging
|
|
30
|
+
*/
|
|
31
|
+
override emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
|
|
32
|
+
if (this.debug) {
|
|
33
|
+
console.debug(`[EventBus] Emitting event: ${String(event)}`, data);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Track event metrics
|
|
37
|
+
const count = this.eventCounts.get(event) || 0;
|
|
38
|
+
this.eventCounts.set(event, count + 1);
|
|
39
|
+
this.lastEventTimes.set(event, Date.now());
|
|
40
|
+
|
|
41
|
+
super.emit(event, data);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get event statistics
|
|
46
|
+
*/
|
|
47
|
+
getEventStats(): { event: string; count: number; lastEmitted: Date | null }[] {
|
|
48
|
+
const stats: { event: string; count: number; lastEmitted: Date | null }[] = [];
|
|
49
|
+
|
|
50
|
+
for (const [event, count] of this.eventCounts.entries()) {
|
|
51
|
+
const lastTime = this.lastEventTimes.get(event);
|
|
52
|
+
stats.push({
|
|
53
|
+
event: String(event),
|
|
54
|
+
count,
|
|
55
|
+
lastEmitted: lastTime ? new Date(lastTime) : null,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return stats.sort((a, b) => b.count - a.count);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reset event statistics
|
|
64
|
+
*/
|
|
65
|
+
resetStats(): void {
|
|
66
|
+
this.eventCounts.clear();
|
|
67
|
+
this.lastEventTimes.clear();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Global event bus for system-wide communication
|
|
73
|
+
*/
|
|
74
|
+
export class EventBus implements IEventBus {
|
|
75
|
+
private static instance: EventBus;
|
|
76
|
+
private typedBus: TypedEventBus;
|
|
77
|
+
|
|
78
|
+
private constructor(debug = false) {
|
|
79
|
+
this.typedBus = new TypedEventBus(debug);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Gets the singleton instance of the event bus
|
|
84
|
+
*/
|
|
85
|
+
static getInstance(debug = false): EventBus {
|
|
86
|
+
if (!EventBus.instance) {
|
|
87
|
+
EventBus.instance = new EventBus(debug);
|
|
88
|
+
}
|
|
89
|
+
return EventBus.instance;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Emits an event
|
|
94
|
+
*/
|
|
95
|
+
emit(event: string, data?: unknown): void {
|
|
96
|
+
// Type-safe emission for known events
|
|
97
|
+
if (event in SystemEvents) {
|
|
98
|
+
this.typedBus.emit(event as keyof EventMap, data as any);
|
|
99
|
+
} else {
|
|
100
|
+
// For custom events, emit as-is
|
|
101
|
+
this.typedBus.emit(event as any, data as any);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Registers an event handler
|
|
107
|
+
*/
|
|
108
|
+
on(event: string, handler: (data: unknown) => void): void {
|
|
109
|
+
this.typedBus.on(event as any, handler);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Removes an event handler
|
|
114
|
+
*/
|
|
115
|
+
off(event: string, handler: (data: unknown) => void): void {
|
|
116
|
+
this.typedBus.off(event as any, handler);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Registers a one-time event handler
|
|
121
|
+
*/
|
|
122
|
+
once(event: string, handler: (data: unknown) => void): void {
|
|
123
|
+
this.typedBus.once(event as any, handler);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Waits for an event to occur
|
|
128
|
+
*/
|
|
129
|
+
async waitFor(event: string, timeoutMs?: number): Promise<unknown> {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const handler = (data: unknown) => {
|
|
132
|
+
if (timer) clearTimeout(timer);
|
|
133
|
+
resolve(data);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
let timer: number | undefined;
|
|
137
|
+
if (timeoutMs) {
|
|
138
|
+
timer = setTimeout(() => {
|
|
139
|
+
this.off(event, handler);
|
|
140
|
+
reject(new Error(`Timeout waiting for event: ${event}`));
|
|
141
|
+
}, timeoutMs);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.once(event, handler);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Creates a filtered event listener
|
|
150
|
+
*/
|
|
151
|
+
onFiltered(
|
|
152
|
+
event: string,
|
|
153
|
+
filter: (data: unknown) => boolean,
|
|
154
|
+
handler: (data: unknown) => void,
|
|
155
|
+
): void {
|
|
156
|
+
this.on(event, (data) => {
|
|
157
|
+
if (filter(data)) {
|
|
158
|
+
handler(data);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get event statistics
|
|
165
|
+
*/
|
|
166
|
+
getEventStats(): { event: string; count: number; lastEmitted: Date | null }[] {
|
|
167
|
+
return this.typedBus.getEventStats();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Reset event statistics
|
|
172
|
+
*/
|
|
173
|
+
resetStats(): void {
|
|
174
|
+
this.typedBus.resetStats();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Remove all listeners for an event
|
|
179
|
+
*/
|
|
180
|
+
removeAllListeners(event?: string): void {
|
|
181
|
+
this.typedBus.removeAllListeners(event as any);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Export singleton instance
|
|
186
|
+
export const eventBus = EventBus.getInstance();
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-based persistence layer for Claude-Flow
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { join } from "https://deno.land/std@0.224.0/path/mod.ts";
|
|
6
|
+
import { ensureDir, exists } from "https://deno.land/std@0.224.0/fs/mod.ts";
|
|
7
|
+
|
|
8
|
+
export interface PersistedAgent {
|
|
9
|
+
id: string;
|
|
10
|
+
type: string;
|
|
11
|
+
name: string;
|
|
12
|
+
status: string;
|
|
13
|
+
capabilities: string[];
|
|
14
|
+
systemPrompt: string;
|
|
15
|
+
maxConcurrentTasks: number;
|
|
16
|
+
priority: number;
|
|
17
|
+
createdAt: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PersistedTask {
|
|
21
|
+
id: string;
|
|
22
|
+
type: string;
|
|
23
|
+
description: string;
|
|
24
|
+
status: string;
|
|
25
|
+
priority: number;
|
|
26
|
+
dependencies: string[];
|
|
27
|
+
metadata: Record<string, unknown>;
|
|
28
|
+
assignedAgent?: string;
|
|
29
|
+
progress: number;
|
|
30
|
+
error?: string;
|
|
31
|
+
createdAt: number;
|
|
32
|
+
completedAt?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface PersistenceData {
|
|
36
|
+
agents: PersistedAgent[];
|
|
37
|
+
tasks: PersistedTask[];
|
|
38
|
+
lastUpdated: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class JsonPersistenceManager {
|
|
42
|
+
private dataPath: string;
|
|
43
|
+
private data: PersistenceData;
|
|
44
|
+
|
|
45
|
+
constructor(dataDir: string = "./memory") {
|
|
46
|
+
this.dataPath = join(dataDir, "claude-flow-data.json");
|
|
47
|
+
this.data = {
|
|
48
|
+
agents: [],
|
|
49
|
+
tasks: [],
|
|
50
|
+
lastUpdated: Date.now(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async initialize(): Promise<void> {
|
|
55
|
+
// Ensure directory exists
|
|
56
|
+
await ensureDir(join(this.dataPath, ".."));
|
|
57
|
+
|
|
58
|
+
// Load existing data if available
|
|
59
|
+
if (await exists(this.dataPath)) {
|
|
60
|
+
try {
|
|
61
|
+
const content = await Deno.readTextFile(this.dataPath);
|
|
62
|
+
this.data = JSON.parse(content);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error("Failed to load persistence data:", error);
|
|
65
|
+
// Keep default empty data
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async save(): Promise<void> {
|
|
71
|
+
this.data.lastUpdated = Date.now();
|
|
72
|
+
await Deno.writeTextFile(this.dataPath, JSON.stringify(this.data, null, 2));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Agent operations
|
|
76
|
+
async saveAgent(agent: PersistedAgent): Promise<void> {
|
|
77
|
+
// Remove existing agent if updating
|
|
78
|
+
this.data.agents = this.data.agents.filter(a => a.id !== agent.id);
|
|
79
|
+
this.data.agents.push(agent);
|
|
80
|
+
await this.save();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async getAgent(id: string): Promise<PersistedAgent | null> {
|
|
84
|
+
return this.data.agents.find(a => a.id === id) || null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getActiveAgents(): Promise<PersistedAgent[]> {
|
|
88
|
+
return this.data.agents.filter(a => a.status === 'active' || a.status === 'idle');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async getAllAgents(): Promise<PersistedAgent[]> {
|
|
92
|
+
return this.data.agents;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async updateAgentStatus(id: string, status: string): Promise<void> {
|
|
96
|
+
const agent = this.data.agents.find(a => a.id === id);
|
|
97
|
+
if (agent) {
|
|
98
|
+
agent.status = status;
|
|
99
|
+
await this.save();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Task operations
|
|
104
|
+
async saveTask(task: PersistedTask): Promise<void> {
|
|
105
|
+
// Remove existing task if updating
|
|
106
|
+
this.data.tasks = this.data.tasks.filter(t => t.id !== task.id);
|
|
107
|
+
this.data.tasks.push(task);
|
|
108
|
+
await this.save();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async getTask(id: string): Promise<PersistedTask | null> {
|
|
112
|
+
return this.data.tasks.find(t => t.id === id) || null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async getActiveTasks(): Promise<PersistedTask[]> {
|
|
116
|
+
return this.data.tasks.filter(t =>
|
|
117
|
+
t.status === 'pending' ||
|
|
118
|
+
t.status === 'in_progress' ||
|
|
119
|
+
t.status === 'assigned'
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async getAllTasks(): Promise<PersistedTask[]> {
|
|
124
|
+
return this.data.tasks;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async updateTaskStatus(id: string, status: string, assignedAgent?: string): Promise<void> {
|
|
128
|
+
const task = this.data.tasks.find(t => t.id === id);
|
|
129
|
+
if (task) {
|
|
130
|
+
task.status = status;
|
|
131
|
+
if (assignedAgent !== undefined) {
|
|
132
|
+
task.assignedAgent = assignedAgent;
|
|
133
|
+
}
|
|
134
|
+
if (status === 'completed') {
|
|
135
|
+
task.completedAt = Date.now();
|
|
136
|
+
}
|
|
137
|
+
await this.save();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async updateTaskProgress(id: string, progress: number): Promise<void> {
|
|
142
|
+
const task = this.data.tasks.find(t => t.id === id);
|
|
143
|
+
if (task) {
|
|
144
|
+
task.progress = progress;
|
|
145
|
+
await this.save();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Statistics
|
|
150
|
+
async getStats(): Promise<{
|
|
151
|
+
totalAgents: number;
|
|
152
|
+
activeAgents: number;
|
|
153
|
+
totalTasks: number;
|
|
154
|
+
pendingTasks: number;
|
|
155
|
+
completedTasks: number;
|
|
156
|
+
}> {
|
|
157
|
+
const activeAgents = this.data.agents.filter(a =>
|
|
158
|
+
a.status === 'active' || a.status === 'idle'
|
|
159
|
+
).length;
|
|
160
|
+
|
|
161
|
+
const pendingTasks = this.data.tasks.filter(t =>
|
|
162
|
+
t.status === 'pending' ||
|
|
163
|
+
t.status === 'in_progress' ||
|
|
164
|
+
t.status === 'assigned'
|
|
165
|
+
).length;
|
|
166
|
+
|
|
167
|
+
const completedTasks = this.data.tasks.filter(t =>
|
|
168
|
+
t.status === 'completed'
|
|
169
|
+
).length;
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
totalAgents: this.data.agents.length,
|
|
173
|
+
activeAgents,
|
|
174
|
+
totalTasks: this.data.tasks.length,
|
|
175
|
+
pendingTasks,
|
|
176
|
+
completedTasks,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
close(): void {
|
|
181
|
+
// No-op for JSON persistence
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging infrastructure for Claude-Flow
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { LoggingConfig } from '../utils/types.ts';
|
|
6
|
+
import { formatBytes } from '../utils/helpers.ts';
|
|
7
|
+
|
|
8
|
+
export interface ILogger {
|
|
9
|
+
debug(message: string, meta?: unknown): void;
|
|
10
|
+
info(message: string, meta?: unknown): void;
|
|
11
|
+
warn(message: string, meta?: unknown): void;
|
|
12
|
+
error(message: string, error?: unknown): void;
|
|
13
|
+
configure(config: LoggingConfig): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export enum LogLevel {
|
|
17
|
+
DEBUG = 0,
|
|
18
|
+
INFO = 1,
|
|
19
|
+
WARN = 2,
|
|
20
|
+
ERROR = 3,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface LogEntry {
|
|
24
|
+
timestamp: string;
|
|
25
|
+
level: string;
|
|
26
|
+
message: string;
|
|
27
|
+
context: Record<string, unknown>;
|
|
28
|
+
data?: unknown;
|
|
29
|
+
error?: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Logger implementation with context support
|
|
34
|
+
*/
|
|
35
|
+
export class Logger implements ILogger {
|
|
36
|
+
private static instance: Logger;
|
|
37
|
+
private config: LoggingConfig;
|
|
38
|
+
private context: Record<string, unknown>;
|
|
39
|
+
private fileHandle?: Deno.FsFile;
|
|
40
|
+
private currentFileSize = 0;
|
|
41
|
+
private currentFileIndex = 0;
|
|
42
|
+
|
|
43
|
+
constructor(
|
|
44
|
+
config: LoggingConfig = {
|
|
45
|
+
level: 'info',
|
|
46
|
+
format: 'json',
|
|
47
|
+
destination: 'console',
|
|
48
|
+
},
|
|
49
|
+
context: Record<string, unknown> = {},
|
|
50
|
+
) {
|
|
51
|
+
this.config = config;
|
|
52
|
+
this.context = context;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Gets the singleton instance of the logger
|
|
57
|
+
*/
|
|
58
|
+
static getInstance(config?: LoggingConfig): Logger {
|
|
59
|
+
if (!Logger.instance) {
|
|
60
|
+
Logger.instance = new Logger(config);
|
|
61
|
+
}
|
|
62
|
+
return Logger.instance;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Updates logger configuration
|
|
67
|
+
*/
|
|
68
|
+
async configure(config: LoggingConfig): Promise<void> {
|
|
69
|
+
this.config = config;
|
|
70
|
+
|
|
71
|
+
// Reset file handle if destination changed
|
|
72
|
+
if (this.fileHandle && config.destination !== 'file' && config.destination !== 'both') {
|
|
73
|
+
await this.fileHandle.close();
|
|
74
|
+
delete this.fileHandle;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
debug(message: string, meta?: unknown): void {
|
|
79
|
+
this.log(LogLevel.DEBUG, message, meta);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
info(message: string, meta?: unknown): void {
|
|
83
|
+
this.log(LogLevel.INFO, message, meta);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
warn(message: string, meta?: unknown): void {
|
|
87
|
+
this.log(LogLevel.WARN, message, meta);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
error(message: string, error?: unknown): void {
|
|
91
|
+
this.log(LogLevel.ERROR, message, undefined, error);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Creates a child logger with additional context
|
|
96
|
+
*/
|
|
97
|
+
child(context: Record<string, unknown>): Logger {
|
|
98
|
+
return new Logger(this.config, { ...this.context, ...context });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private log(level: LogLevel, message: string, data?: unknown, error?: unknown): void {
|
|
102
|
+
if (!this.shouldLog(level)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const entry: LogEntry = {
|
|
107
|
+
timestamp: new Date().toISOString(),
|
|
108
|
+
level: LogLevel[level],
|
|
109
|
+
message,
|
|
110
|
+
context: this.context,
|
|
111
|
+
data,
|
|
112
|
+
error,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const formatted = this.format(entry);
|
|
116
|
+
|
|
117
|
+
if (this.config.destination === 'console' || this.config.destination === 'both') {
|
|
118
|
+
this.writeToConsole(level, formatted);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (this.config.destination === 'file' || this.config.destination === 'both') {
|
|
122
|
+
this.writeToFile(formatted);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private shouldLog(level: LogLevel): boolean {
|
|
127
|
+
const configLevel = LogLevel[this.config.level.toUpperCase() as keyof typeof LogLevel];
|
|
128
|
+
return level >= configLevel;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private format(entry: LogEntry): string {
|
|
132
|
+
if (this.config.format === 'json') {
|
|
133
|
+
return JSON.stringify(entry);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Text format
|
|
137
|
+
const contextStr = Object.keys(entry.context).length > 0
|
|
138
|
+
? ` ${JSON.stringify(entry.context)}`
|
|
139
|
+
: '';
|
|
140
|
+
const dataStr = entry.data !== undefined
|
|
141
|
+
? ` ${JSON.stringify(entry.data)}`
|
|
142
|
+
: '';
|
|
143
|
+
const errorStr = entry.error !== undefined
|
|
144
|
+
? entry.error instanceof Error
|
|
145
|
+
? `\n Error: ${entry.error.message}\n Stack: ${entry.error.stack}`
|
|
146
|
+
: ` Error: ${JSON.stringify(entry.error)}`
|
|
147
|
+
: '';
|
|
148
|
+
|
|
149
|
+
return `[${entry.timestamp}] ${entry.level} ${entry.message}${contextStr}${dataStr}${errorStr}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private writeToConsole(level: LogLevel, message: string): void {
|
|
153
|
+
switch (level) {
|
|
154
|
+
case LogLevel.DEBUG:
|
|
155
|
+
console.debug(message);
|
|
156
|
+
break;
|
|
157
|
+
case LogLevel.INFO:
|
|
158
|
+
console.info(message);
|
|
159
|
+
break;
|
|
160
|
+
case LogLevel.WARN:
|
|
161
|
+
console.warn(message);
|
|
162
|
+
break;
|
|
163
|
+
case LogLevel.ERROR:
|
|
164
|
+
console.error(message);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private async writeToFile(message: string): Promise<void> {
|
|
170
|
+
if (!this.config.filePath) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
// Check if we need to rotate the log file
|
|
176
|
+
if (await this.shouldRotate()) {
|
|
177
|
+
await this.rotate();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Open file handle if not already open
|
|
181
|
+
if (!this.fileHandle) {
|
|
182
|
+
this.fileHandle = await Deno.open(this.config.filePath, {
|
|
183
|
+
create: true,
|
|
184
|
+
append: true,
|
|
185
|
+
write: true,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Write the message
|
|
190
|
+
const encoder = new TextEncoder();
|
|
191
|
+
const data = encoder.encode(message + '\n');
|
|
192
|
+
await this.fileHandle.write(data);
|
|
193
|
+
this.currentFileSize += data.length;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error('Failed to write to log file:', error);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private async shouldRotate(): Promise<boolean> {
|
|
200
|
+
if (!this.config.maxFileSize || !this.fileHandle) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const stat = await this.fileHandle.stat();
|
|
206
|
+
return stat.size >= this.config.maxFileSize;
|
|
207
|
+
} catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private async rotate(): Promise<void> {
|
|
213
|
+
if (!this.config.filePath || !this.config.maxFiles) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Close current file
|
|
218
|
+
if (this.fileHandle) {
|
|
219
|
+
await this.fileHandle.close();
|
|
220
|
+
delete this.fileHandle;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Rename current file
|
|
224
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
225
|
+
const rotatedPath = `${this.config.filePath}.${timestamp}`;
|
|
226
|
+
await Deno.rename(this.config.filePath, rotatedPath);
|
|
227
|
+
|
|
228
|
+
// Clean up old files
|
|
229
|
+
await this.cleanupOldFiles();
|
|
230
|
+
|
|
231
|
+
// Reset file size
|
|
232
|
+
this.currentFileSize = 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private async cleanupOldFiles(): Promise<void> {
|
|
236
|
+
if (!this.config.filePath || !this.config.maxFiles) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const dir = this.config.filePath.substring(0, this.config.filePath.lastIndexOf('/'));
|
|
241
|
+
const baseFileName = this.config.filePath.substring(this.config.filePath.lastIndexOf('/') + 1);
|
|
242
|
+
|
|
243
|
+
const files: string[] = [];
|
|
244
|
+
for await (const entry of Deno.readDir(dir)) {
|
|
245
|
+
if (entry.isFile && entry.name.startsWith(baseFileName + '.')) {
|
|
246
|
+
files.push(entry.name);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Sort files by timestamp (newest first)
|
|
251
|
+
files.sort().reverse();
|
|
252
|
+
|
|
253
|
+
// Remove old files
|
|
254
|
+
const filesToRemove = files.slice(this.config.maxFiles - 1);
|
|
255
|
+
for (const file of filesToRemove) {
|
|
256
|
+
await Deno.remove(`${dir}/${file}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Export singleton instance
|
|
262
|
+
export const logger = Logger.getInstance();
|