@johpaz/hive-sdk 0.0.14 → 0.0.15
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/.github/CODEOWNERS +9 -0
- package/.github/workflows/publish.yml +89 -0
- package/.github/workflows/version-bump.yml +102 -0
- package/CHANGELOG.md +38 -0
- package/README.md +158 -0
- package/bun.lock +543 -0
- package/bunfig.toml +7 -0
- package/docs/API-AGENTS.md +316 -0
- package/docs/API-CONTEXT-COMPILER.md +252 -0
- package/docs/API-DAG-SCHEDULER.md +273 -0
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +293 -0
- package/docs/API-WORKERS-EVENTS.md +152 -0
- package/docs/INDEX.md +141 -0
- package/docs/README.md +68 -0
- package/package.json +54 -105
- package/packages/cli/package.json +17 -0
- package/packages/cli/src/commands/init.ts +56 -0
- package/packages/cli/src/commands/run.ts +45 -0
- package/packages/cli/src/commands/test.ts +42 -0
- package/packages/cli/src/commands/trace.ts +55 -0
- package/packages/cli/src/index.ts +43 -0
- package/packages/core/package.json +58 -0
- package/packages/core/src/ace/Curator.ts +158 -0
- package/packages/core/src/ace/Reflector.ts +200 -0
- package/packages/core/src/ace/Tracer.ts +100 -0
- package/packages/core/src/ace/index.ts +4 -0
- package/packages/core/src/agent/AgentRunner.ts +699 -0
- package/packages/core/src/agent/Compaction.ts +221 -0
- package/packages/core/src/agent/ContextCompiler.ts +567 -0
- package/packages/core/src/agent/ContextGuard.ts +91 -0
- package/packages/core/src/agent/ConversationStore.ts +244 -0
- package/packages/core/src/agent/Hooks.ts +166 -0
- package/packages/core/src/agent/NativeTools.ts +31 -0
- package/packages/core/src/agent/PromptBuilder.ts +169 -0
- package/packages/core/src/agent/Service.ts +267 -0
- package/packages/core/src/agent/StuckLoop.ts +133 -0
- package/packages/core/src/agent/index.ts +12 -0
- package/packages/core/src/agent/providers/LLMClient.ts +149 -0
- package/packages/core/src/agent/providers/anthropic.ts +212 -0
- package/packages/core/src/agent/providers/gemini.ts +215 -0
- package/packages/core/src/agent/providers/index.ts +199 -0
- package/packages/core/src/agent/providers/interface.ts +195 -0
- package/packages/core/src/agent/providers/ollama.ts +175 -0
- package/packages/core/src/agent/providers/openai-compat.ts +231 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +147 -0
- package/packages/core/src/agent/selectors/SkillSelector.ts +478 -0
- package/packages/core/src/agent/selectors/ToolSelector.ts +577 -0
- package/packages/core/src/agent/selectors/index.ts +6 -0
- package/packages/core/src/api/createAgent.test.ts +48 -0
- package/packages/core/src/api/createAgent.ts +122 -0
- package/packages/core/src/api/index.ts +2 -0
- package/packages/core/src/canvas/CanvasManager.ts +390 -0
- package/packages/core/src/canvas/a2ui-tools.ts +255 -0
- package/packages/core/src/canvas/canvas-tools.ts +448 -0
- package/packages/core/src/canvas/emitter.ts +149 -0
- package/packages/core/src/canvas/index.ts +6 -0
- package/packages/core/src/config/index.ts +2 -0
- package/packages/core/src/config/loader.ts +554 -0
- package/packages/core/src/ethics/EthicsGuard.test.ts +54 -0
- package/packages/core/src/ethics/EthicsGuard.ts +66 -0
- package/packages/core/src/ethics/index.ts +2 -0
- package/packages/core/src/gateway/channel-notify.test.ts +14 -0
- package/packages/core/src/gateway/channel-notify.ts +12 -0
- package/packages/core/src/gateway/index.ts +1 -0
- package/packages/core/src/index.ts +37 -0
- package/packages/core/src/mcp/MCPClient.ts +439 -0
- package/packages/core/src/mcp/MCPToolAdapter.ts +176 -0
- package/packages/core/src/mcp/config.ts +13 -0
- package/packages/core/src/mcp/hot-reload.ts +147 -0
- package/packages/core/src/mcp/index.ts +11 -0
- package/packages/core/src/mcp/logger.ts +42 -0
- package/packages/core/src/mcp/singleton.ts +21 -0
- package/packages/core/src/mcp/transports/index.ts +67 -0
- package/packages/core/src/mcp/transports/sse.ts +241 -0
- package/packages/core/src/mcp/transports/websocket.ts +159 -0
- package/packages/core/src/memory/Scratchpad.test.ts +47 -0
- package/packages/core/src/memory/Scratchpad.ts +37 -0
- package/packages/core/src/memory/Storage.ts +6 -0
- package/packages/core/src/memory/index.ts +2 -0
- package/packages/core/src/multimodal/VisionService.ts +293 -0
- package/packages/core/src/multimodal/index.ts +2 -0
- package/packages/core/src/multimodal/types.ts +28 -0
- package/packages/core/src/security/Pairing.ts +250 -0
- package/packages/core/src/security/RateLimit.ts +270 -0
- package/packages/core/src/security/index.ts +4 -0
- package/packages/core/src/skills/SkillLoader.ts +388 -0
- package/packages/core/src/skills/bundled-data.generated.ts +3332 -0
- package/packages/core/src/skills/defineSkill.ts +18 -0
- package/packages/core/src/skills/index.ts +4 -0
- package/packages/core/src/state/index.ts +2 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/SQLiteStorage.ts +407 -0
- package/packages/core/src/storage/crypto.ts +101 -0
- package/packages/core/src/storage/index.ts +10 -0
- package/packages/core/src/storage/onboarding.ts +1603 -0
- package/packages/core/src/storage/schema.ts +689 -0
- package/packages/core/src/storage/seed.ts +740 -0
- package/packages/core/src/storage/usage.ts +374 -0
- package/packages/core/src/swarm/AgentBus.ts +460 -0
- package/packages/core/src/swarm/AgentExecutor.ts +53 -0
- package/packages/core/src/swarm/Coordinator.ts +251 -0
- package/packages/core/src/swarm/EventBridge.ts +122 -0
- package/packages/core/src/swarm/EventBus.ts +169 -0
- package/packages/core/src/swarm/TaskGraph.ts +192 -0
- package/packages/core/src/swarm/TaskNode.ts +97 -0
- package/packages/core/src/swarm/TaskResult.ts +22 -0
- package/packages/core/src/swarm/WorkerPool.ts +236 -0
- package/packages/core/src/swarm/errors.ts +37 -0
- package/packages/core/src/swarm/index.ts +30 -0
- package/packages/core/src/swarm/presets/HiveLearnPreset.ts +99 -0
- package/packages/core/src/swarm/presets/ResearchPreset.ts +97 -0
- package/packages/core/src/swarm/presets/index.ts +4 -0
- package/packages/core/src/swarm/strategies/ParallelStrategy.ts +21 -0
- package/packages/core/src/swarm/strategies/PriorityStrategy.ts +46 -0
- package/packages/core/src/swarm/strategies/index.ts +3 -0
- package/packages/core/src/swarm/types.ts +164 -0
- package/packages/core/src/tools/ToolExecutor.ts +58 -0
- package/packages/core/src/tools/ToolRegistry.test.ts +98 -0
- package/packages/core/src/tools/ToolRegistry.ts +61 -0
- package/packages/core/src/tools/agents/get-available-models.ts +118 -0
- package/packages/core/src/tools/agents/index.ts +715 -0
- package/packages/core/src/tools/bridge-events.ts +26 -0
- package/packages/core/src/tools/canvas/index.ts +375 -0
- package/packages/core/src/tools/cli/index.ts +142 -0
- package/packages/core/src/tools/codebridge/index.ts +342 -0
- package/packages/core/src/tools/core/index.ts +476 -0
- package/packages/core/src/tools/cron/index.ts +626 -0
- package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
- package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
- package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
- package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
- package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
- package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
- package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
- package/packages/core/src/tools/filesystem/index.ts +34 -0
- package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
- package/packages/core/src/tools/index.ts +231 -0
- package/packages/core/src/tools/meeting/index.ts +363 -0
- package/packages/core/src/tools/office/index.ts +47 -0
- package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
- package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
- package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
- package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
- package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
- package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
- package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
- package/packages/core/src/tools/projects/index.ts +37 -0
- package/packages/core/src/tools/projects/project-create.ts +94 -0
- package/packages/core/src/tools/projects/project-done.ts +66 -0
- package/packages/core/src/tools/projects/project-fail.ts +66 -0
- package/packages/core/src/tools/projects/project-list.ts +96 -0
- package/packages/core/src/tools/projects/project-update.ts +72 -0
- package/packages/core/src/tools/projects/task-create.ts +68 -0
- package/packages/core/src/tools/projects/task-evaluate.ts +93 -0
- package/packages/core/src/tools/projects/task-update.ts +93 -0
- package/packages/core/src/tools/types.ts +39 -0
- package/packages/core/src/tools/voice/index.ts +104 -0
- package/packages/core/src/tools/web/browser-click.ts +78 -0
- package/packages/core/src/tools/web/browser-extract.ts +139 -0
- package/packages/core/src/tools/web/browser-navigate.ts +106 -0
- package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
- package/packages/core/src/tools/web/browser-script.ts +88 -0
- package/packages/core/src/tools/web/browser-service.ts +554 -0
- package/packages/core/src/tools/web/browser-type.ts +101 -0
- package/packages/core/src/tools/web/browser-wait.ts +136 -0
- package/packages/core/src/tools/web/index.ts +41 -0
- package/packages/core/src/tools/web/web-fetch.ts +78 -0
- package/packages/core/src/tools/web/web-search.ts +123 -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 +10 -0
- package/packages/core/src/utils/logger.ts +389 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/utils/toon.ts +253 -0
- package/packages/core/src/voice/index.ts +656 -0
- package/test/setup-db.ts +216 -0
- package/tsconfig.json +39 -0
- package/src/agents.ts +0 -1
- package/src/canvas.ts +0 -1
- package/src/channels.ts +0 -1
- package/src/config.ts +0 -1
- package/src/events.ts +0 -1
- package/src/gateway.ts +0 -1
- package/src/index.ts +0 -304
- package/src/mcp.ts +0 -1
- package/src/multimodal.ts +0 -1
- package/src/scheduler.ts +0 -1
- package/src/security.ts +0 -1
- package/src/skills.ts +0 -1
- package/src/state.ts +0 -1
- package/src/storage.ts +0 -1
- package/src/tools.ts +0 -1
- package/src/tts.ts +0 -1
- package/src/types.ts +0 -82
- package/src/utils.ts +0 -1
- package/src/voice.ts +0 -1
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { mkdirSync, unlinkSync, renameSync, existsSync } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getHiveDir, loadConfig } 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
|
+
const config = loadConfig();
|
|
372
|
+
_logger = new Logger({ level: config.logging?.level });
|
|
373
|
+
}
|
|
374
|
+
return _logger;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export const logger = {
|
|
378
|
+
child: (opts: any) => getLogger().child(opts),
|
|
379
|
+
debug: (msg: string, meta?: unknown) => getLogger().debug(msg, meta),
|
|
380
|
+
info: (msg: string, meta?: unknown) => getLogger().info(msg, meta),
|
|
381
|
+
warn: (msg: string, meta?: unknown) => getLogger().warn(msg, meta),
|
|
382
|
+
error: (msg: string, meta?: unknown) => getLogger().error(msg, meta),
|
|
383
|
+
setCorrelationContext: (ctx: any) => getLogger().setCorrelationContext(ctx),
|
|
384
|
+
clearCorrelationContext: () => getLogger().clearCorrelationContext(),
|
|
385
|
+
getCorrelationId: () => getLogger().getCorrelationId(),
|
|
386
|
+
withCorrelationId: (id: string) => getLogger().withCorrelationId(id),
|
|
387
|
+
setLevel: (level: any) => getLogger().setLevel(level),
|
|
388
|
+
setHandler: (handler: any) => { /* no-op for compatibility */ },
|
|
389
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOON Format Utility - Direct lib usage with native compression analysis
|
|
3
|
+
*
|
|
4
|
+
* TOON (Token-Oriented Object Notation) provides ~40% token savings vs JSON.
|
|
5
|
+
* Uses toon-format-parser library directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { encode, decode, analyzeCompression } from 'toon-format-parser'
|
|
9
|
+
import { logger } from './logger'
|
|
10
|
+
import { recordToonSavings } from '../storage/usage.ts'
|
|
11
|
+
|
|
12
|
+
const log = logger.child('toon')
|
|
13
|
+
|
|
14
|
+
export interface ToonStringifyResult {
|
|
15
|
+
content: string
|
|
16
|
+
format: 'toon'
|
|
17
|
+
originalSize: number
|
|
18
|
+
toonSize: number
|
|
19
|
+
tokensSaved: number
|
|
20
|
+
savingsPercent: number
|
|
21
|
+
costSaved: number
|
|
22
|
+
// Complete compression metrics
|
|
23
|
+
jsonBytes: number
|
|
24
|
+
toonBytes: number
|
|
25
|
+
savedBytes: number
|
|
26
|
+
savedPercent: number
|
|
27
|
+
jsonTokens: number
|
|
28
|
+
toonTokens: number
|
|
29
|
+
savedTokensPercent: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Estimate tokens in text (standard: ~4 chars per token)
|
|
34
|
+
*/
|
|
35
|
+
export function estimateTokens(text: string): number {
|
|
36
|
+
if (!text) return 0
|
|
37
|
+
return Math.ceil(text.length / 4)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Average cost per token for TOON savings calculation
|
|
42
|
+
* Based on Gemini 3 Flash pricing: $0.15/1M input + $0.60/1M output = $0.375/1M average
|
|
43
|
+
* This is used as a baseline for calculating USD savings from token compression
|
|
44
|
+
*/
|
|
45
|
+
const TOON_AVERAGE_COST_PER_TOKEN = 0.000000375 // $0.375 per million tokens
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Stringify JavaScript object to TOON format with token savings calculation
|
|
49
|
+
* Uses native analyzeCompression from toon-format-parser
|
|
50
|
+
*/
|
|
51
|
+
export function stringify(data: any, model?: string): ToonStringifyResult {
|
|
52
|
+
const jsonContent = JSON.stringify(data)
|
|
53
|
+
const originalSize = jsonContent.length
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const toonContent = encode(data)
|
|
57
|
+
const toonSize = toonContent.length
|
|
58
|
+
|
|
59
|
+
// Use native analyzeCompression for accurate metrics
|
|
60
|
+
const analysis = analyzeCompression(data)
|
|
61
|
+
const tokensSaved = Math.max(0, analysis.savedTokens)
|
|
62
|
+
const savingsPercent = Math.max(0, analysis.savedTokensPercent)
|
|
63
|
+
|
|
64
|
+
// Calculate cost savings using average cost (Gemini 3 Flash baseline)
|
|
65
|
+
const costSaved = tokensSaved * TOON_AVERAGE_COST_PER_TOKEN
|
|
66
|
+
|
|
67
|
+
log.debug(`[TOON] Converted - saved ${tokensSaved} tokens ($${costSaved.toFixed(6)}) (${savingsPercent.toFixed(1)}%)`)
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
content: toonContent,
|
|
71
|
+
format: 'toon',
|
|
72
|
+
originalSize,
|
|
73
|
+
toonSize,
|
|
74
|
+
tokensSaved,
|
|
75
|
+
savingsPercent,
|
|
76
|
+
costSaved,
|
|
77
|
+
// Complete compression metrics from analyzeCompression
|
|
78
|
+
jsonBytes: analysis.jsonBytes,
|
|
79
|
+
toonBytes: analysis.toonBytes,
|
|
80
|
+
savedBytes: analysis.savedBytes,
|
|
81
|
+
savedPercent: analysis.savedPercent,
|
|
82
|
+
jsonTokens: analysis.jsonTokens,
|
|
83
|
+
toonTokens: analysis.toonTokens,
|
|
84
|
+
savedTokensPercent: analysis.savedTokensPercent,
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
log.warn(`[TOON] Failed, falling back to JSON:`, error)
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
content: jsonContent,
|
|
91
|
+
format: 'toon',
|
|
92
|
+
originalSize,
|
|
93
|
+
toonSize: originalSize,
|
|
94
|
+
tokensSaved: 0,
|
|
95
|
+
savingsPercent: 0,
|
|
96
|
+
costSaved: 0,
|
|
97
|
+
jsonBytes: originalSize,
|
|
98
|
+
toonBytes: originalSize,
|
|
99
|
+
savedBytes: 0,
|
|
100
|
+
savedPercent: 0,
|
|
101
|
+
jsonTokens: estimateTokens(jsonContent),
|
|
102
|
+
toonTokens: estimateTokens(jsonContent),
|
|
103
|
+
savedTokensPercent: 0,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Format tool result to TOON (for LLM consumption)
|
|
110
|
+
* Records savings in DB if model is provided
|
|
111
|
+
*/
|
|
112
|
+
export function formatToolResult(data: any, model?: string): string {
|
|
113
|
+
const result = stringify(data, model)
|
|
114
|
+
|
|
115
|
+
if (result.tokensSaved > 0 && model) {
|
|
116
|
+
recordToonSavings({
|
|
117
|
+
jsonBytes: result.jsonBytes,
|
|
118
|
+
toonBytes: result.toonBytes,
|
|
119
|
+
savedBytes: result.savedBytes,
|
|
120
|
+
savedPercent: result.savedPercent,
|
|
121
|
+
jsonTokens: result.jsonTokens,
|
|
122
|
+
toonTokens: result.toonTokens,
|
|
123
|
+
savedTokens: result.tokensSaved,
|
|
124
|
+
savedTokensPercent: result.savedTokensPercent,
|
|
125
|
+
}, result.costSaved, 'tool_result')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return result.content
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Format MCP response to TOON
|
|
133
|
+
*/
|
|
134
|
+
export function formatMCPResponse(data: any, model?: string): string {
|
|
135
|
+
const result = stringify(data, model)
|
|
136
|
+
|
|
137
|
+
if (result.tokensSaved > 0 && model) {
|
|
138
|
+
recordToonSavings({
|
|
139
|
+
jsonBytes: result.jsonBytes,
|
|
140
|
+
toonBytes: result.toonBytes,
|
|
141
|
+
savedBytes: result.savedBytes,
|
|
142
|
+
savedPercent: result.savedPercent,
|
|
143
|
+
jsonTokens: result.jsonTokens,
|
|
144
|
+
toonTokens: result.toonTokens,
|
|
145
|
+
savedTokens: result.tokensSaved,
|
|
146
|
+
savedTokensPercent: result.savedTokensPercent,
|
|
147
|
+
}, result.costSaved, 'mcp_response')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result.content
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Format skill output to TOON
|
|
155
|
+
*/
|
|
156
|
+
export function formatSkillOutput(data: any, model?: string): string {
|
|
157
|
+
const result = stringify(data, model)
|
|
158
|
+
|
|
159
|
+
if (result.tokensSaved > 0 && model) {
|
|
160
|
+
recordToonSavings({
|
|
161
|
+
jsonBytes: result.jsonBytes,
|
|
162
|
+
toonBytes: result.toonBytes,
|
|
163
|
+
savedBytes: result.savedBytes,
|
|
164
|
+
savedPercent: result.savedPercent,
|
|
165
|
+
jsonTokens: result.jsonTokens,
|
|
166
|
+
toonTokens: result.toonTokens,
|
|
167
|
+
savedTokens: result.tokensSaved,
|
|
168
|
+
savedTokensPercent: result.savedTokensPercent,
|
|
169
|
+
}, result.costSaved, 'skill_output')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return result.content
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Format context data (ethics, notes, projects, user data) to TOON
|
|
177
|
+
*/
|
|
178
|
+
export function formatContext(data: any, model?: string): string {
|
|
179
|
+
const result = stringify(data, model)
|
|
180
|
+
|
|
181
|
+
if (result.tokensSaved > 0 && model) {
|
|
182
|
+
recordToonSavings({
|
|
183
|
+
jsonBytes: result.jsonBytes,
|
|
184
|
+
toonBytes: result.toonBytes,
|
|
185
|
+
savedBytes: result.savedBytes,
|
|
186
|
+
savedPercent: result.savedPercent,
|
|
187
|
+
jsonTokens: result.jsonTokens,
|
|
188
|
+
toonTokens: result.toonTokens,
|
|
189
|
+
savedTokens: result.tokensSaved,
|
|
190
|
+
savedTokensPercent: result.savedTokensPercent,
|
|
191
|
+
}, result.costSaved, 'context')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return result.content
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Middleware wrapper for tool execution with TOON formatting
|
|
199
|
+
*/
|
|
200
|
+
export async function withToonFormat<T>(
|
|
201
|
+
toolName: string,
|
|
202
|
+
fn: () => Promise<T>,
|
|
203
|
+
model?: string
|
|
204
|
+
): Promise<string> {
|
|
205
|
+
const t0 = performance.now()
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const result = await fn()
|
|
209
|
+
const duration = Math.round(performance.now() - t0)
|
|
210
|
+
|
|
211
|
+
const toonResult = formatToolResult(result, model)
|
|
212
|
+
|
|
213
|
+
log.debug(`[TOON] Tool ${toolName} executed in ${duration}ms - output converted`)
|
|
214
|
+
|
|
215
|
+
return toonResult
|
|
216
|
+
} catch (error) {
|
|
217
|
+
const errorObj = {
|
|
218
|
+
error: true,
|
|
219
|
+
tool: toolName,
|
|
220
|
+
message: error instanceof Error ? error.message : String(error),
|
|
221
|
+
timestamp: new Date().toISOString(),
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return formatToolResult(errorObj, model)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Print compression report to console for debugging/analysis
|
|
230
|
+
* Note: This prints directly to console, does not return a string
|
|
231
|
+
*/
|
|
232
|
+
export function reportCompression(data: any): void {
|
|
233
|
+
// Import dynamically to avoid issues if function doesn't exist
|
|
234
|
+
import('toon-format-parser')
|
|
235
|
+
.then(({ printCompressionReport }) => {
|
|
236
|
+
printCompressionReport(data)
|
|
237
|
+
})
|
|
238
|
+
.catch(() => {
|
|
239
|
+
// Fallback: manual report
|
|
240
|
+
const analysis = analyzeCompression(data)
|
|
241
|
+
console.log(`\nTOON Compression Report:`)
|
|
242
|
+
console.log(` JSON: ${analysis.jsonBytes} bytes, ~${analysis.jsonTokens} tokens`)
|
|
243
|
+
console.log(` TOON: ${analysis.toonBytes} bytes, ~${analysis.toonTokens} tokens`)
|
|
244
|
+
console.log(` Saved: ${analysis.savedBytes} bytes, ${analysis.savedTokens} tokens (${analysis.savedPercent.toFixed(1)}%)`)
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get compression analysis as an object (for programmatic use)
|
|
250
|
+
*/
|
|
251
|
+
export function getCompressionAnalysis(data: any): ReturnType<typeof analyzeCompression> {
|
|
252
|
+
return analyzeCompression(data)
|
|
253
|
+
}
|