@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.
Files changed (199) hide show
  1. package/.github/CODEOWNERS +9 -0
  2. package/.github/workflows/publish.yml +89 -0
  3. package/.github/workflows/version-bump.yml +102 -0
  4. package/CHANGELOG.md +38 -0
  5. package/README.md +158 -0
  6. package/bun.lock +543 -0
  7. package/bunfig.toml +7 -0
  8. package/docs/API-AGENTS.md +316 -0
  9. package/docs/API-CONTEXT-COMPILER.md +252 -0
  10. package/docs/API-DAG-SCHEDULER.md +273 -0
  11. package/docs/API-TOOLS-SKILLS-CHANNELS.md +293 -0
  12. package/docs/API-WORKERS-EVENTS.md +152 -0
  13. package/docs/INDEX.md +141 -0
  14. package/docs/README.md +68 -0
  15. package/package.json +54 -105
  16. package/packages/cli/package.json +17 -0
  17. package/packages/cli/src/commands/init.ts +56 -0
  18. package/packages/cli/src/commands/run.ts +45 -0
  19. package/packages/cli/src/commands/test.ts +42 -0
  20. package/packages/cli/src/commands/trace.ts +55 -0
  21. package/packages/cli/src/index.ts +43 -0
  22. package/packages/core/package.json +58 -0
  23. package/packages/core/src/ace/Curator.ts +158 -0
  24. package/packages/core/src/ace/Reflector.ts +200 -0
  25. package/packages/core/src/ace/Tracer.ts +100 -0
  26. package/packages/core/src/ace/index.ts +4 -0
  27. package/packages/core/src/agent/AgentRunner.ts +699 -0
  28. package/packages/core/src/agent/Compaction.ts +221 -0
  29. package/packages/core/src/agent/ContextCompiler.ts +567 -0
  30. package/packages/core/src/agent/ContextGuard.ts +91 -0
  31. package/packages/core/src/agent/ConversationStore.ts +244 -0
  32. package/packages/core/src/agent/Hooks.ts +166 -0
  33. package/packages/core/src/agent/NativeTools.ts +31 -0
  34. package/packages/core/src/agent/PromptBuilder.ts +169 -0
  35. package/packages/core/src/agent/Service.ts +267 -0
  36. package/packages/core/src/agent/StuckLoop.ts +133 -0
  37. package/packages/core/src/agent/index.ts +12 -0
  38. package/packages/core/src/agent/providers/LLMClient.ts +149 -0
  39. package/packages/core/src/agent/providers/anthropic.ts +212 -0
  40. package/packages/core/src/agent/providers/gemini.ts +215 -0
  41. package/packages/core/src/agent/providers/index.ts +199 -0
  42. package/packages/core/src/agent/providers/interface.ts +195 -0
  43. package/packages/core/src/agent/providers/ollama.ts +175 -0
  44. package/packages/core/src/agent/providers/openai-compat.ts +231 -0
  45. package/packages/core/src/agent/providers.ts +1 -0
  46. package/packages/core/src/agent/selectors/PlaybookSelector.ts +147 -0
  47. package/packages/core/src/agent/selectors/SkillSelector.ts +478 -0
  48. package/packages/core/src/agent/selectors/ToolSelector.ts +577 -0
  49. package/packages/core/src/agent/selectors/index.ts +6 -0
  50. package/packages/core/src/api/createAgent.test.ts +48 -0
  51. package/packages/core/src/api/createAgent.ts +122 -0
  52. package/packages/core/src/api/index.ts +2 -0
  53. package/packages/core/src/canvas/CanvasManager.ts +390 -0
  54. package/packages/core/src/canvas/a2ui-tools.ts +255 -0
  55. package/packages/core/src/canvas/canvas-tools.ts +448 -0
  56. package/packages/core/src/canvas/emitter.ts +149 -0
  57. package/packages/core/src/canvas/index.ts +6 -0
  58. package/packages/core/src/config/index.ts +2 -0
  59. package/packages/core/src/config/loader.ts +554 -0
  60. package/packages/core/src/ethics/EthicsGuard.test.ts +54 -0
  61. package/packages/core/src/ethics/EthicsGuard.ts +66 -0
  62. package/packages/core/src/ethics/index.ts +2 -0
  63. package/packages/core/src/gateway/channel-notify.test.ts +14 -0
  64. package/packages/core/src/gateway/channel-notify.ts +12 -0
  65. package/packages/core/src/gateway/index.ts +1 -0
  66. package/packages/core/src/index.ts +37 -0
  67. package/packages/core/src/mcp/MCPClient.ts +439 -0
  68. package/packages/core/src/mcp/MCPToolAdapter.ts +176 -0
  69. package/packages/core/src/mcp/config.ts +13 -0
  70. package/packages/core/src/mcp/hot-reload.ts +147 -0
  71. package/packages/core/src/mcp/index.ts +11 -0
  72. package/packages/core/src/mcp/logger.ts +42 -0
  73. package/packages/core/src/mcp/singleton.ts +21 -0
  74. package/packages/core/src/mcp/transports/index.ts +67 -0
  75. package/packages/core/src/mcp/transports/sse.ts +241 -0
  76. package/packages/core/src/mcp/transports/websocket.ts +159 -0
  77. package/packages/core/src/memory/Scratchpad.test.ts +47 -0
  78. package/packages/core/src/memory/Scratchpad.ts +37 -0
  79. package/packages/core/src/memory/Storage.ts +6 -0
  80. package/packages/core/src/memory/index.ts +2 -0
  81. package/packages/core/src/multimodal/VisionService.ts +293 -0
  82. package/packages/core/src/multimodal/index.ts +2 -0
  83. package/packages/core/src/multimodal/types.ts +28 -0
  84. package/packages/core/src/security/Pairing.ts +250 -0
  85. package/packages/core/src/security/RateLimit.ts +270 -0
  86. package/packages/core/src/security/index.ts +4 -0
  87. package/packages/core/src/skills/SkillLoader.ts +388 -0
  88. package/packages/core/src/skills/bundled-data.generated.ts +3332 -0
  89. package/packages/core/src/skills/defineSkill.ts +18 -0
  90. package/packages/core/src/skills/index.ts +4 -0
  91. package/packages/core/src/state/index.ts +2 -0
  92. package/packages/core/src/state/store.ts +312 -0
  93. package/packages/core/src/storage/SQLiteStorage.ts +407 -0
  94. package/packages/core/src/storage/crypto.ts +101 -0
  95. package/packages/core/src/storage/index.ts +10 -0
  96. package/packages/core/src/storage/onboarding.ts +1603 -0
  97. package/packages/core/src/storage/schema.ts +689 -0
  98. package/packages/core/src/storage/seed.ts +740 -0
  99. package/packages/core/src/storage/usage.ts +374 -0
  100. package/packages/core/src/swarm/AgentBus.ts +460 -0
  101. package/packages/core/src/swarm/AgentExecutor.ts +53 -0
  102. package/packages/core/src/swarm/Coordinator.ts +251 -0
  103. package/packages/core/src/swarm/EventBridge.ts +122 -0
  104. package/packages/core/src/swarm/EventBus.ts +169 -0
  105. package/packages/core/src/swarm/TaskGraph.ts +192 -0
  106. package/packages/core/src/swarm/TaskNode.ts +97 -0
  107. package/packages/core/src/swarm/TaskResult.ts +22 -0
  108. package/packages/core/src/swarm/WorkerPool.ts +236 -0
  109. package/packages/core/src/swarm/errors.ts +37 -0
  110. package/packages/core/src/swarm/index.ts +30 -0
  111. package/packages/core/src/swarm/presets/HiveLearnPreset.ts +99 -0
  112. package/packages/core/src/swarm/presets/ResearchPreset.ts +97 -0
  113. package/packages/core/src/swarm/presets/index.ts +4 -0
  114. package/packages/core/src/swarm/strategies/ParallelStrategy.ts +21 -0
  115. package/packages/core/src/swarm/strategies/PriorityStrategy.ts +46 -0
  116. package/packages/core/src/swarm/strategies/index.ts +3 -0
  117. package/packages/core/src/swarm/types.ts +164 -0
  118. package/packages/core/src/tools/ToolExecutor.ts +58 -0
  119. package/packages/core/src/tools/ToolRegistry.test.ts +98 -0
  120. package/packages/core/src/tools/ToolRegistry.ts +61 -0
  121. package/packages/core/src/tools/agents/get-available-models.ts +118 -0
  122. package/packages/core/src/tools/agents/index.ts +715 -0
  123. package/packages/core/src/tools/bridge-events.ts +26 -0
  124. package/packages/core/src/tools/canvas/index.ts +375 -0
  125. package/packages/core/src/tools/cli/index.ts +142 -0
  126. package/packages/core/src/tools/codebridge/index.ts +342 -0
  127. package/packages/core/src/tools/core/index.ts +476 -0
  128. package/packages/core/src/tools/cron/index.ts +626 -0
  129. package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
  130. package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
  131. package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
  132. package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
  133. package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
  134. package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
  135. package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
  136. package/packages/core/src/tools/filesystem/index.ts +34 -0
  137. package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
  138. package/packages/core/src/tools/index.ts +231 -0
  139. package/packages/core/src/tools/meeting/index.ts +363 -0
  140. package/packages/core/src/tools/office/index.ts +47 -0
  141. package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
  142. package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
  143. package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
  144. package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
  145. package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
  146. package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
  147. package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
  148. package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
  149. package/packages/core/src/tools/projects/index.ts +37 -0
  150. package/packages/core/src/tools/projects/project-create.ts +94 -0
  151. package/packages/core/src/tools/projects/project-done.ts +66 -0
  152. package/packages/core/src/tools/projects/project-fail.ts +66 -0
  153. package/packages/core/src/tools/projects/project-list.ts +96 -0
  154. package/packages/core/src/tools/projects/project-update.ts +72 -0
  155. package/packages/core/src/tools/projects/task-create.ts +68 -0
  156. package/packages/core/src/tools/projects/task-evaluate.ts +93 -0
  157. package/packages/core/src/tools/projects/task-update.ts +93 -0
  158. package/packages/core/src/tools/types.ts +39 -0
  159. package/packages/core/src/tools/voice/index.ts +104 -0
  160. package/packages/core/src/tools/web/browser-click.ts +78 -0
  161. package/packages/core/src/tools/web/browser-extract.ts +139 -0
  162. package/packages/core/src/tools/web/browser-navigate.ts +106 -0
  163. package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
  164. package/packages/core/src/tools/web/browser-script.ts +88 -0
  165. package/packages/core/src/tools/web/browser-service.ts +554 -0
  166. package/packages/core/src/tools/web/browser-type.ts +101 -0
  167. package/packages/core/src/tools/web/browser-wait.ts +136 -0
  168. package/packages/core/src/tools/web/index.ts +41 -0
  169. package/packages/core/src/tools/web/web-fetch.ts +78 -0
  170. package/packages/core/src/tools/web/web-search.ts +123 -0
  171. package/packages/core/src/utils/benchmark.ts +80 -0
  172. package/packages/core/src/utils/crypto.ts +73 -0
  173. package/packages/core/src/utils/date.ts +42 -0
  174. package/packages/core/src/utils/index.ts +10 -0
  175. package/packages/core/src/utils/logger.ts +389 -0
  176. package/packages/core/src/utils/retry.ts +70 -0
  177. package/packages/core/src/utils/toon.ts +253 -0
  178. package/packages/core/src/voice/index.ts +656 -0
  179. package/test/setup-db.ts +216 -0
  180. package/tsconfig.json +39 -0
  181. package/src/agents.ts +0 -1
  182. package/src/canvas.ts +0 -1
  183. package/src/channels.ts +0 -1
  184. package/src/config.ts +0 -1
  185. package/src/events.ts +0 -1
  186. package/src/gateway.ts +0 -1
  187. package/src/index.ts +0 -304
  188. package/src/mcp.ts +0 -1
  189. package/src/multimodal.ts +0 -1
  190. package/src/scheduler.ts +0 -1
  191. package/src/security.ts +0 -1
  192. package/src/skills.ts +0 -1
  193. package/src/state.ts +0 -1
  194. package/src/storage.ts +0 -1
  195. package/src/tools.ts +0 -1
  196. package/src/tts.ts +0 -1
  197. package/src/types.ts +0 -82
  198. package/src/utils.ts +0 -1
  199. 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
+ }