skyloom 1.4.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.
Files changed (225) hide show
  1. package/.github/workflows/ci.yml +36 -0
  2. package/CONVERSION_PLAN.md +191 -0
  3. package/README.md +67 -0
  4. package/dist/agents/dew.d.ts +15 -0
  5. package/dist/agents/dew.d.ts.map +1 -0
  6. package/dist/agents/dew.js +74 -0
  7. package/dist/agents/dew.js.map +1 -0
  8. package/dist/agents/fair.d.ts +15 -0
  9. package/dist/agents/fair.d.ts.map +1 -0
  10. package/dist/agents/fair.js +106 -0
  11. package/dist/agents/fair.js.map +1 -0
  12. package/dist/agents/fog.d.ts +15 -0
  13. package/dist/agents/fog.d.ts.map +1 -0
  14. package/dist/agents/fog.js +52 -0
  15. package/dist/agents/fog.js.map +1 -0
  16. package/dist/agents/frost.d.ts +15 -0
  17. package/dist/agents/frost.d.ts.map +1 -0
  18. package/dist/agents/frost.js +54 -0
  19. package/dist/agents/frost.js.map +1 -0
  20. package/dist/agents/rain.d.ts +15 -0
  21. package/dist/agents/rain.d.ts.map +1 -0
  22. package/dist/agents/rain.js +54 -0
  23. package/dist/agents/rain.js.map +1 -0
  24. package/dist/agents/snow.d.ts +27 -0
  25. package/dist/agents/snow.d.ts.map +1 -0
  26. package/dist/agents/snow.js +226 -0
  27. package/dist/agents/snow.js.map +1 -0
  28. package/dist/cli/main.d.ts +7 -0
  29. package/dist/cli/main.d.ts.map +1 -0
  30. package/dist/cli/main.js +402 -0
  31. package/dist/cli/main.js.map +1 -0
  32. package/dist/cli/mode.d.ts +17 -0
  33. package/dist/cli/mode.d.ts.map +1 -0
  34. package/dist/cli/mode.js +56 -0
  35. package/dist/cli/mode.js.map +1 -0
  36. package/dist/core/agent.d.ts +174 -0
  37. package/dist/core/agent.d.ts.map +1 -0
  38. package/dist/core/agent.js +1332 -0
  39. package/dist/core/agent.js.map +1 -0
  40. package/dist/core/agent_helpers.d.ts +51 -0
  41. package/dist/core/agent_helpers.d.ts.map +1 -0
  42. package/dist/core/agent_helpers.js +477 -0
  43. package/dist/core/agent_helpers.js.map +1 -0
  44. package/dist/core/bus.d.ts +99 -0
  45. package/dist/core/bus.d.ts.map +1 -0
  46. package/dist/core/bus.js +191 -0
  47. package/dist/core/bus.js.map +1 -0
  48. package/dist/core/cache.d.ts +63 -0
  49. package/dist/core/cache.d.ts.map +1 -0
  50. package/dist/core/cache.js +121 -0
  51. package/dist/core/cache.js.map +1 -0
  52. package/dist/core/checkpoint.d.ts +19 -0
  53. package/dist/core/checkpoint.d.ts.map +1 -0
  54. package/dist/core/checkpoint.js +120 -0
  55. package/dist/core/checkpoint.js.map +1 -0
  56. package/dist/core/circuit_breaker.d.ts +46 -0
  57. package/dist/core/circuit_breaker.d.ts.map +1 -0
  58. package/dist/core/circuit_breaker.js +99 -0
  59. package/dist/core/circuit_breaker.js.map +1 -0
  60. package/dist/core/config.d.ts +97 -0
  61. package/dist/core/config.d.ts.map +1 -0
  62. package/dist/core/config.js +281 -0
  63. package/dist/core/config.js.map +1 -0
  64. package/dist/core/constants.d.ts +78 -0
  65. package/dist/core/constants.d.ts.map +1 -0
  66. package/dist/core/constants.js +84 -0
  67. package/dist/core/constants.js.map +1 -0
  68. package/dist/core/factory.d.ts +63 -0
  69. package/dist/core/factory.d.ts.map +1 -0
  70. package/dist/core/factory.js +537 -0
  71. package/dist/core/factory.js.map +1 -0
  72. package/dist/core/icons.d.ts +28 -0
  73. package/dist/core/icons.d.ts.map +1 -0
  74. package/dist/core/icons.js +86 -0
  75. package/dist/core/icons.js.map +1 -0
  76. package/dist/core/index.d.ts +29 -0
  77. package/dist/core/index.d.ts.map +1 -0
  78. package/dist/core/index.js +54 -0
  79. package/dist/core/index.js.map +1 -0
  80. package/dist/core/llm.d.ts +121 -0
  81. package/dist/core/llm.d.ts.map +1 -0
  82. package/dist/core/llm.js +532 -0
  83. package/dist/core/llm.js.map +1 -0
  84. package/dist/core/logger.d.ts +57 -0
  85. package/dist/core/logger.d.ts.map +1 -0
  86. package/dist/core/logger.js +122 -0
  87. package/dist/core/logger.js.map +1 -0
  88. package/dist/core/mcp.d.ts +190 -0
  89. package/dist/core/mcp.d.ts.map +1 -0
  90. package/dist/core/mcp.js +822 -0
  91. package/dist/core/mcp.js.map +1 -0
  92. package/dist/core/mcp_server.d.ts +26 -0
  93. package/dist/core/mcp_server.d.ts.map +1 -0
  94. package/dist/core/mcp_server.js +211 -0
  95. package/dist/core/mcp_server.js.map +1 -0
  96. package/dist/core/memory.d.ts +190 -0
  97. package/dist/core/memory.d.ts.map +1 -0
  98. package/dist/core/memory.js +988 -0
  99. package/dist/core/memory.js.map +1 -0
  100. package/dist/core/middleware.d.ts +114 -0
  101. package/dist/core/middleware.d.ts.map +1 -0
  102. package/dist/core/middleware.js +248 -0
  103. package/dist/core/middleware.js.map +1 -0
  104. package/dist/core/pipelines.d.ts +87 -0
  105. package/dist/core/pipelines.d.ts.map +1 -0
  106. package/dist/core/pipelines.js +301 -0
  107. package/dist/core/pipelines.js.map +1 -0
  108. package/dist/core/profile.d.ts +23 -0
  109. package/dist/core/profile.d.ts.map +1 -0
  110. package/dist/core/profile.js +289 -0
  111. package/dist/core/profile.js.map +1 -0
  112. package/dist/core/router.d.ts +24 -0
  113. package/dist/core/router.d.ts.map +1 -0
  114. package/dist/core/router.js +111 -0
  115. package/dist/core/router.js.map +1 -0
  116. package/dist/core/schemas.d.ts +82 -0
  117. package/dist/core/schemas.d.ts.map +1 -0
  118. package/dist/core/schemas.js +200 -0
  119. package/dist/core/schemas.js.map +1 -0
  120. package/dist/core/semantic.d.ts +92 -0
  121. package/dist/core/semantic.d.ts.map +1 -0
  122. package/dist/core/semantic.js +175 -0
  123. package/dist/core/semantic.js.map +1 -0
  124. package/dist/core/skill.d.ts +68 -0
  125. package/dist/core/skill.d.ts.map +1 -0
  126. package/dist/core/skill.js +350 -0
  127. package/dist/core/skill.js.map +1 -0
  128. package/dist/core/tool.d.ts +99 -0
  129. package/dist/core/tool.d.ts.map +1 -0
  130. package/dist/core/tool.js +341 -0
  131. package/dist/core/tool.js.map +1 -0
  132. package/dist/core/tool_router.d.ts +29 -0
  133. package/dist/core/tool_router.d.ts.map +1 -0
  134. package/dist/core/tool_router.js +172 -0
  135. package/dist/core/tool_router.js.map +1 -0
  136. package/dist/core/workspace.d.ts +48 -0
  137. package/dist/core/workspace.d.ts.map +1 -0
  138. package/dist/core/workspace.js +179 -0
  139. package/dist/core/workspace.js.map +1 -0
  140. package/dist/plugins/loader.d.ts +17 -0
  141. package/dist/plugins/loader.d.ts.map +1 -0
  142. package/dist/plugins/loader.js +96 -0
  143. package/dist/plugins/loader.js.map +1 -0
  144. package/dist/skills/loader.d.ts +9 -0
  145. package/dist/skills/loader.d.ts.map +1 -0
  146. package/dist/skills/loader.js +78 -0
  147. package/dist/skills/loader.js.map +1 -0
  148. package/dist/tools/builtin.d.ts +10 -0
  149. package/dist/tools/builtin.d.ts.map +1 -0
  150. package/dist/tools/builtin.js +414 -0
  151. package/dist/tools/builtin.js.map +1 -0
  152. package/dist/tools/computer.d.ts +12 -0
  153. package/dist/tools/computer.d.ts.map +1 -0
  154. package/dist/tools/computer.js +326 -0
  155. package/dist/tools/computer.js.map +1 -0
  156. package/dist/tools/delegate.d.ts +10 -0
  157. package/dist/tools/delegate.d.ts.map +1 -0
  158. package/dist/tools/delegate.js +45 -0
  159. package/dist/tools/delegate.js.map +1 -0
  160. package/dist/web/server.d.ts +5 -0
  161. package/dist/web/server.d.ts.map +1 -0
  162. package/dist/web/server.js +647 -0
  163. package/dist/web/server.js.map +1 -0
  164. package/dist/web/tts.d.ts +33 -0
  165. package/dist/web/tts.d.ts.map +1 -0
  166. package/dist/web/tts.js +69 -0
  167. package/dist/web/tts.js.map +1 -0
  168. package/package.json +60 -0
  169. package/scripts/install.js +48 -0
  170. package/scripts/link.js +10 -0
  171. package/setup.bat +79 -0
  172. package/skill-test-ty2fOA/test.md +10 -0
  173. package/src/agents/dew.ts +70 -0
  174. package/src/agents/fair.ts +102 -0
  175. package/src/agents/fog.ts +48 -0
  176. package/src/agents/frost.ts +50 -0
  177. package/src/agents/rain.ts +50 -0
  178. package/src/agents/snow.ts +239 -0
  179. package/src/cli/main.ts +405 -0
  180. package/src/cli/mode.ts +58 -0
  181. package/src/core/agent.ts +1506 -0
  182. package/src/core/agent_helpers.ts +461 -0
  183. package/src/core/bus.ts +221 -0
  184. package/src/core/cache.ts +153 -0
  185. package/src/core/checkpoint.ts +94 -0
  186. package/src/core/circuit_breaker.ts +119 -0
  187. package/src/core/config.ts +341 -0
  188. package/src/core/constants.ts +95 -0
  189. package/src/core/factory.ts +627 -0
  190. package/src/core/icons.ts +53 -0
  191. package/src/core/index.ts +31 -0
  192. package/src/core/llm.ts +724 -0
  193. package/src/core/logger.ts +144 -0
  194. package/src/core/mcp.ts +953 -0
  195. package/src/core/mcp_server.ts +176 -0
  196. package/src/core/memory.ts +1169 -0
  197. package/src/core/middleware.ts +350 -0
  198. package/src/core/pipelines.ts +424 -0
  199. package/src/core/profile.ts +255 -0
  200. package/src/core/router.ts +124 -0
  201. package/src/core/schemas.ts +282 -0
  202. package/src/core/semantic.ts +211 -0
  203. package/src/core/skill.ts +342 -0
  204. package/src/core/tool.ts +427 -0
  205. package/src/core/tool_router.ts +193 -0
  206. package/src/core/workspace.ts +150 -0
  207. package/src/plugins/loader.ts +66 -0
  208. package/src/skills/loader.ts +46 -0
  209. package/src/sql.js.d.ts +29 -0
  210. package/src/tools/builtin.ts +382 -0
  211. package/src/tools/computer.ts +269 -0
  212. package/src/tools/delegate.ts +49 -0
  213. package/src/web/server.ts +634 -0
  214. package/src/web/tts.ts +93 -0
  215. package/tests/bus.test.ts +121 -0
  216. package/tests/icons.test.ts +45 -0
  217. package/tests/router.test.ts +86 -0
  218. package/tests/schemas.test.ts +51 -0
  219. package/tests/semantic.test.ts +83 -0
  220. package/tests/setup.ts +10 -0
  221. package/tests/skill.test.ts +172 -0
  222. package/tests/tool.test.ts +108 -0
  223. package/tests/tool_router.test.ts +71 -0
  224. package/tsconfig.json +37 -0
  225. package/vitest.config.ts +17 -0
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Lightweight LRU cache for LLM responses.
3
+ *
4
+ * Deduplicates identical requests within a configurable time window.
5
+ * Keyed by (model, messages_json) hash.
6
+ */
7
+
8
+ import crypto from 'crypto';
9
+
10
+ /**
11
+ * Cache entry containing timestamp and response.
12
+ */
13
+ interface CacheEntry {
14
+ timestamp: number;
15
+ response: string;
16
+ }
17
+
18
+ /**
19
+ * LRU cache for LLM completions with TTL expiration.
20
+ */
21
+ export class LLMCache {
22
+ private maxSize: number;
23
+ private ttlSeconds: number;
24
+ private cache: Map<string, CacheEntry> = new Map();
25
+
26
+ constructor(maxSize: number = 128, ttlSeconds: number = 60) {
27
+ this.maxSize = maxSize;
28
+ this.ttlSeconds = ttlSeconds;
29
+ }
30
+
31
+ /**
32
+ * Generate a cache key from model, messages, and parameters.
33
+ *
34
+ * Uses SHA256 hash for deterministic, compact keys. Parameters are sorted
35
+ * to ensure consistent keys regardless of key order.
36
+ */
37
+ private makeKey(
38
+ model: string,
39
+ messages: Record<string, any>[],
40
+ params?: Record<string, any>
41
+ ): string {
42
+ const raw = JSON.stringify([model, messages, params || {}], this.jsonReplacer);
43
+ return crypto.createHash('sha256').update(raw).digest('hex').slice(0, 32);
44
+ }
45
+
46
+ /**
47
+ * JSON replacer for consistent serialization.
48
+ */
49
+ private jsonReplacer(_key: string, value: any): any {
50
+ if (typeof value === 'function') {
51
+ return undefined;
52
+ }
53
+ return value;
54
+ }
55
+
56
+ /**
57
+ * Get a cached response if it exists and hasn't expired.
58
+ *
59
+ * @param model - LLM model name
60
+ * @param messages - Conversation messages
61
+ * @param params - Optional parameters
62
+ * @returns Cached response or null if miss/expired
63
+ */
64
+ get(
65
+ model: string,
66
+ messages: Record<string, any>[],
67
+ params?: Record<string, any>
68
+ ): string | null {
69
+ const key = this.makeKey(model, messages, params);
70
+ const entry = this.cache.get(key);
71
+
72
+ if (!entry) {
73
+ return null;
74
+ }
75
+
76
+ // Check if expired
77
+ const now = Date.now();
78
+ const ageSeconds = (now - entry.timestamp) / 1000;
79
+
80
+ if (ageSeconds >= this.ttlSeconds) {
81
+ this.cache.delete(key);
82
+ return null;
83
+ }
84
+
85
+ // Move to end (LRU)
86
+ this.cache.delete(key);
87
+ this.cache.set(key, entry);
88
+
89
+ return entry.response;
90
+ }
91
+
92
+ /**
93
+ * Cache a response.
94
+ *
95
+ * Refuses to cache empty or very short content to avoid bloat.
96
+ * Also performs LRU eviction if cache exceeds max_size.
97
+ *
98
+ * @param model - LLM model name
99
+ * @param messages - Conversation messages
100
+ * @param response - LLM response to cache
101
+ * @param params - Optional parameters
102
+ */
103
+ set(
104
+ model: string,
105
+ messages: Record<string, any>[],
106
+ response: string,
107
+ params?: Record<string, any>
108
+ ): void {
109
+ // Don't cache empty or very short responses
110
+ if (!response || response.length < 10) {
111
+ return;
112
+ }
113
+
114
+ const key = this.makeKey(model, messages, params);
115
+ const entry: CacheEntry = {
116
+ timestamp: Date.now(),
117
+ response,
118
+ };
119
+
120
+ this.cache.delete(key);
121
+ this.cache.set(key, entry);
122
+
123
+ // Evict oldest entries if over capacity
124
+ while (this.cache.size > this.maxSize) {
125
+ const firstKey = this.cache.keys().next().value;
126
+ if (firstKey !== undefined) this.cache.delete(firstKey);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Clear all cache entries.
132
+ */
133
+ clear(): void {
134
+ this.cache.clear();
135
+ }
136
+
137
+ /**
138
+ * Get current cache size.
139
+ */
140
+ get size(): number {
141
+ return this.cache.size;
142
+ }
143
+
144
+ /**
145
+ * Get cache hit rate statistics.
146
+ */
147
+ getStats(): { size: number; maxSize: number } {
148
+ return {
149
+ size: this.cache.size,
150
+ maxSize: this.maxSize,
151
+ };
152
+ }
153
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Orchestration checkpoint — save/restore task state.
3
+ *
4
+ * Writes ~/.skyloom/task_checkpoint.json so a long-running orchestration
5
+ * interrupted by Ctrl-C can be resumed.
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { USER_CONFIG_DIR } from './config';
11
+
12
+ function checkpointPath(): string {
13
+ return path.join(USER_CONFIG_DIR, 'task_checkpoint.json');
14
+ }
15
+
16
+ /**
17
+ * Save current orchestration state so it can be resumed later.
18
+ */
19
+ export function save(
20
+ goal: string,
21
+ tasks: any[],
22
+ results: any[],
23
+ completedIds?: Set<string>
24
+ ): void {
25
+ const cids = completedIds || new Set(results.map((r: any) => r.id));
26
+ const payload = {
27
+ goal,
28
+ tasks: tasks.map(serializeTask),
29
+ results: results.map(serializeResult),
30
+ completed_ids: Array.from(cids).sort(),
31
+ };
32
+
33
+ const p = checkpointPath();
34
+ const dir = path.dirname(p);
35
+ if (!fs.existsSync(dir)) {
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ }
38
+
39
+ const tmp = p + '.tmp';
40
+ fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
41
+ fs.renameSync(tmp, p);
42
+ }
43
+
44
+ /**
45
+ * Return the last saved checkpoint dict, or null if none / unreadable.
46
+ */
47
+ export function load(): Record<string, any> | null {
48
+ const p = checkpointPath();
49
+ if (!fs.existsSync(p)) {
50
+ return null;
51
+ }
52
+ try {
53
+ const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
54
+ return typeof data === 'object' && data !== null ? data : null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Delete the checkpoint file.
62
+ */
63
+ export function clear(): void {
64
+ try {
65
+ const p = checkpointPath();
66
+ if (fs.existsSync(p)) {
67
+ fs.unlinkSync(p);
68
+ }
69
+ } catch {
70
+ // Ignore cleanup errors
71
+ }
72
+ }
73
+
74
+ // ── Serialization helpers ──
75
+
76
+ function serializeTask(t: any): Record<string, any> {
77
+ return {
78
+ id: t.id,
79
+ description: t.description,
80
+ assigned_to: t.assignedTo ?? t.assigned_to,
81
+ all_deps: t.allDeps ?? t.all_deps ?? [],
82
+ status: t.status?.value ?? t.status ?? 'unknown',
83
+ };
84
+ }
85
+
86
+ function serializeResult(r: any): Record<string, any> {
87
+ return {
88
+ id: r.id,
89
+ agent: r.agent,
90
+ description: r.description,
91
+ success: r.success,
92
+ content: r.content,
93
+ };
94
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Circuit breaker pattern for fault tolerance
3
+ */
4
+
5
+ export interface CircuitBreakerConfig {
6
+ name: string;
7
+ failureThreshold?: number;
8
+ successThreshold?: number;
9
+ resetTimeout?: number;
10
+ }
11
+
12
+ export type CircuitBreakerState = "closed" | "open" | "half_open";
13
+
14
+ /**
15
+ * Circuit breaker implementation
16
+ */
17
+ export class CircuitBreaker {
18
+ private state: CircuitBreakerState = "closed";
19
+ private failureCount = 0;
20
+ private successCount = 0;
21
+ private lastFailureTime = 0;
22
+ private failureThreshold: number;
23
+ private successThreshold: number;
24
+ private resetTimeout: number;
25
+
26
+ constructor(config: CircuitBreakerConfig) {
27
+ this.failureThreshold = config.failureThreshold ?? 5;
28
+ this.successThreshold = config.successThreshold ?? 3;
29
+ this.resetTimeout = config.resetTimeout ?? 60000; // 60 seconds
30
+ }
31
+
32
+ /**
33
+ * Check if execution is allowed
34
+ */
35
+ canExecute(): boolean {
36
+ if (this.state === "closed") {
37
+ return true;
38
+ }
39
+
40
+ if (this.state === "open") {
41
+ const timeSinceLastFailure = Date.now() - this.lastFailureTime;
42
+ if (timeSinceLastFailure > this.resetTimeout) {
43
+ this.state = "half_open";
44
+ this.successCount = 0;
45
+ return true;
46
+ }
47
+ return false;
48
+ }
49
+
50
+ // half_open state allows attempts
51
+ return true;
52
+ }
53
+
54
+ /**
55
+ * Record a successful execution
56
+ */
57
+ recordSuccess(): void {
58
+ this.failureCount = 0;
59
+
60
+ if (this.state === "half_open") {
61
+ this.successCount++;
62
+ if (this.successCount >= this.successThreshold) {
63
+ this.state = "closed";
64
+ this.successCount = 0;
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Record a failed execution
71
+ */
72
+ recordFailure(): void {
73
+ this.lastFailureTime = Date.now();
74
+ this.failureCount++;
75
+
76
+ if (this.failureCount >= this.failureThreshold) {
77
+ this.state = "open";
78
+ this.failureCount = 0;
79
+ }
80
+
81
+ if (this.state === "half_open") {
82
+ this.state = "open";
83
+ this.failureCount = 0;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Get current state
89
+ */
90
+ getState(): CircuitBreakerState {
91
+ return this.state;
92
+ }
93
+
94
+ /**
95
+ * Reset the circuit breaker
96
+ */
97
+ reset(): void {
98
+ this.state = "closed";
99
+ this.failureCount = 0;
100
+ this.successCount = 0;
101
+ this.lastFailureTime = 0;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Get or create a circuit breaker for a service
107
+ */
108
+ const breakers = new Map<string, CircuitBreaker>();
109
+
110
+ export function getBreaker(name: string, config?: CircuitBreakerConfig): CircuitBreaker {
111
+ if (!breakers.has(name)) {
112
+ breakers.set(name, new CircuitBreaker(config ?? { name }));
113
+ }
114
+ return breakers.get(name)!;
115
+ }
116
+
117
+ export function clearBreakers(): void {
118
+ breakers.clear();
119
+ }
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Configuration management for Skyloom
3
+ */
4
+
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import * as os from "os";
8
+ import * as yaml from "yaml";
9
+ import { getLogger } from "./logger";
10
+
11
+ const log = getLogger("config");
12
+
13
+ /**
14
+ * Configuration directory paths
15
+ */
16
+ const LEGACY_CONFIG_DIR = path.join(os.homedir(), ".weather-agents");
17
+
18
+ /**
19
+ * Resolve the user configuration directory
20
+ * Migrates from legacy ~/.weather-agents to ~/.skyloom if needed
21
+ */
22
+ function resolveUserConfigDir(): string {
23
+ const newDir = path.join(os.homedir(), ".skyloom");
24
+
25
+ if (!fs.existsSync(newDir) && fs.existsSync(LEGACY_CONFIG_DIR)) {
26
+ try {
27
+ fs.renameSync(LEGACY_CONFIG_DIR, newDir);
28
+ log.info("Migrated config directory", {
29
+ from: LEGACY_CONFIG_DIR,
30
+ to: newDir,
31
+ });
32
+ } catch (error) {
33
+ log.warn("Failed to migrate config directory", {
34
+ error: (error as Error).message,
35
+ });
36
+ return LEGACY_CONFIG_DIR;
37
+ }
38
+ }
39
+
40
+ return newDir;
41
+ }
42
+
43
+ export const USER_CONFIG_DIR = resolveUserConfigDir();
44
+
45
+ /**
46
+ * Find the config directory (bundled or user-provided)
47
+ */
48
+ function findConfigDir(): string {
49
+ // Try bundled config directory (relative to src or dist)
50
+ const possiblePaths = [
51
+ path.join(__dirname, "..", "..", "..", "config"),
52
+ path.join(__dirname, "..", "config"),
53
+ path.join(process.cwd(), "config"),
54
+ ];
55
+
56
+ for (const configPath of possiblePaths) {
57
+ if (fs.existsSync(path.join(configPath, "default.yaml"))) {
58
+ return configPath;
59
+ }
60
+ }
61
+
62
+ // Fall back to user config directory
63
+ return path.join(USER_CONFIG_DIR, "config");
64
+ }
65
+
66
+ export const CONFIG_DIR = findConfigDir();
67
+
68
+ /**
69
+ * Memory configuration
70
+ */
71
+ export interface MemoryConfig {
72
+ dbPath: string;
73
+ shortTermLimit: number;
74
+ maxPersistedMessages?: number;
75
+ }
76
+
77
+ /**
78
+ * All agent names — single source of truth
79
+ */
80
+ export const AGENT_NAMES = ["fog", "rain", "frost", "snow", "dew", "fair"] as const;
81
+
82
+ /**
83
+ * Load a YAML file safely
84
+ */
85
+ function loadYaml(filePath: string): Record<string, unknown> | null {
86
+ try {
87
+ if (!fs.existsSync(filePath)) {
88
+ return null;
89
+ }
90
+
91
+ const content = fs.readFileSync(filePath, "utf-8");
92
+ const data = yaml.parse(content);
93
+
94
+ return typeof data === "object" && data !== null ? (data as Record<string, unknown>) : {};
95
+ } catch (error) {
96
+ log.error("Failed to load YAML file", {
97
+ file: filePath,
98
+ error: (error as Error).message,
99
+ });
100
+ return null;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Model catalog entry
106
+ */
107
+ export interface ModelEntry {
108
+ name: string;
109
+ provider?: string;
110
+ context_window?: number;
111
+ max_output?: number;
112
+ input_cost_per_1k?: number;
113
+ output_cost_per_1k?: number;
114
+ fallback?: string[];
115
+ }
116
+
117
+ /**
118
+ * Load available models from models.yaml
119
+ */
120
+ export function loadModelCatalog(): Record<string, ModelEntry[]> {
121
+ const modelPath = path.join(CONFIG_DIR, "models.yaml");
122
+ const data = loadYaml(modelPath);
123
+
124
+ if (!data) {
125
+ return {};
126
+ }
127
+
128
+ const catalog: Record<string, ModelEntry[]> = {};
129
+
130
+ for (const [provider, models] of Object.entries(data)) {
131
+ if (typeof models === "object" && models !== null) {
132
+ catalog[provider] = [];
133
+
134
+ for (const [name, info] of Object.entries(models as Record<string, unknown>)) {
135
+ const entry: ModelEntry = { name };
136
+
137
+ if (typeof info === "object" && info !== null) {
138
+ Object.assign(entry, info);
139
+ } else if (typeof info === "string") {
140
+ entry.provider = info;
141
+ }
142
+
143
+ catalog[provider].push(entry);
144
+ }
145
+ }
146
+ }
147
+
148
+ return catalog;
149
+ }
150
+
151
+ /**
152
+ * Provider catalog entry
153
+ */
154
+ export interface ProviderEntry {
155
+ env_var?: string;
156
+ region?: string;
157
+ docs_url?: string;
158
+ base_url?: string;
159
+ aliases?: string[];
160
+ [key: string]: unknown;
161
+ }
162
+
163
+ let providerCatalogCache: Record<string, ProviderEntry> | null = null;
164
+
165
+ /**
166
+ * Load provider catalog from providers.yaml
167
+ */
168
+ export function loadProviderCatalog(): Record<string, ProviderEntry> {
169
+ if (providerCatalogCache) {
170
+ return providerCatalogCache;
171
+ }
172
+
173
+ const catalog: Record<string, ProviderEntry> = {};
174
+
175
+ // Load bundled providers
176
+ const bundledPath = path.join(CONFIG_DIR, "providers.yaml");
177
+ const bundledData = loadYaml(bundledPath);
178
+
179
+ if (bundledData) {
180
+ for (const [key, value] of Object.entries(bundledData)) {
181
+ if (typeof value === "object" && value !== null) {
182
+ catalog[key] = { ...value } as ProviderEntry;
183
+ }
184
+ }
185
+ }
186
+
187
+ // Load user overrides
188
+ const userPath = path.join(USER_CONFIG_DIR, "providers.yaml");
189
+ const userData = loadYaml(userPath);
190
+
191
+ if (userData) {
192
+ for (const [key, value] of Object.entries(userData)) {
193
+ if (typeof value === "object" && value !== null) {
194
+ catalog[key] = {
195
+ ...catalog[key],
196
+ ...value,
197
+ } as ProviderEntry;
198
+ }
199
+ }
200
+ }
201
+
202
+ providerCatalogCache = catalog;
203
+ return catalog;
204
+ }
205
+
206
+ /**
207
+ * Agent configuration
208
+ */
209
+ export interface AgentConfig {
210
+ model: string;
211
+ provider: string;
212
+ temperature?: number;
213
+ top_p?: number;
214
+ max_tokens?: number;
215
+ system_prompt?: string;
216
+ tools?: string[];
217
+ }
218
+
219
+ /**
220
+ * Skyloom configuration
221
+ */
222
+ export interface SkyloomConfig {
223
+ agents: Record<string, AgentConfig>;
224
+ providers?: Record<string, ProviderEntry>;
225
+ models?: Record<string, ModelEntry[]>;
226
+ }
227
+
228
+ /**
229
+ * Load default configuration
230
+ */
231
+ export function loadDefaultConfig(): SkyloomConfig {
232
+ const defaultPath = path.join(CONFIG_DIR, "default.yaml");
233
+ const data = loadYaml(defaultPath);
234
+
235
+ if (!data) {
236
+ return { agents: {} };
237
+ }
238
+
239
+ return data as unknown as SkyloomConfig;
240
+ }
241
+
242
+ /**
243
+ * Load user configuration (from ~/.skyloom/config.yaml)
244
+ */
245
+ export function loadUserConfig(): SkyloomConfig | null {
246
+ const userPath = path.join(USER_CONFIG_DIR, "config.yaml");
247
+ const data = loadYaml(userPath);
248
+
249
+ if (!data) {
250
+ return null;
251
+ }
252
+
253
+ return data as unknown as SkyloomConfig;
254
+ }
255
+
256
+ /**
257
+ * Merge user config on top of default config
258
+ */
259
+ export function mergeConfigs(defaultCfg: SkyloomConfig, userCfg: SkyloomConfig | null): SkyloomConfig {
260
+ if (!userCfg) {
261
+ return defaultCfg;
262
+ }
263
+
264
+ return {
265
+ agents: {
266
+ ...defaultCfg.agents,
267
+ ...userCfg.agents,
268
+ },
269
+ providers: {
270
+ ...defaultCfg.providers,
271
+ ...userCfg.providers,
272
+ },
273
+ models: {
274
+ ...defaultCfg.models,
275
+ ...userCfg.models,
276
+ },
277
+ };
278
+ }
279
+
280
+ /**
281
+ * Load the complete configuration
282
+ */
283
+ export function loadConfig(): SkyloomConfig {
284
+ const defaultCfg = loadDefaultConfig();
285
+ const userCfg = loadUserConfig();
286
+ return mergeConfigs(defaultCfg, userCfg);
287
+ }
288
+
289
+ /**
290
+ * Save user configuration
291
+ */
292
+ export function saveUserConfig(config: SkyloomConfig): void {
293
+ // Ensure user config directory exists
294
+ if (!fs.existsSync(USER_CONFIG_DIR)) {
295
+ fs.mkdirSync(USER_CONFIG_DIR, { recursive: true });
296
+ }
297
+
298
+ const userPath = path.join(USER_CONFIG_DIR, "config.yaml");
299
+ const content = yaml.stringify(config);
300
+
301
+ fs.writeFileSync(userPath, content, "utf-8");
302
+ log.info("Saved user configuration", { path: userPath });
303
+ }
304
+
305
+ /**
306
+ * Get an agent configuration
307
+ */
308
+ export function getAgentConfig(config: SkyloomConfig, agentName: string): AgentConfig | null {
309
+ return config.agents[agentName] || null;
310
+ }
311
+
312
+ /**
313
+ * Format models for display
314
+ */
315
+ export function formatModelsForDisplay(catalog: Record<string, ModelEntry[]>): string {
316
+ const lines: string[] = [];
317
+
318
+ for (const [provider, models] of Object.entries(catalog)) {
319
+ lines.push(` [${provider.toUpperCase()}]`);
320
+
321
+ for (const m of models) {
322
+ const costParts: string[] = [];
323
+ if (m.input_cost_per_1k) {
324
+ costParts.push(`$${m.input_cost_per_1k.toFixed(4)}/1k in`);
325
+ }
326
+ if (m.output_cost_per_1k) {
327
+ costParts.push(`$${m.output_cost_per_1k.toFixed(4)}/1k out`);
328
+ }
329
+
330
+ const costStr = costParts.length > 0 ? ` cost=(${costParts.join(", ")})` : "";
331
+ const fallbackStr =
332
+ m.fallback && m.fallback.length > 0 ? ` fallback->${m.fallback.join(" > ")}` : "";
333
+
334
+ lines.push(
335
+ ` ${m.name} (ctx=${m.context_window || "?"}, max=${m.max_output || "?"})${costStr}${fallbackStr}`
336
+ );
337
+ }
338
+ }
339
+
340
+ return lines.join("\n");
341
+ }