@deimoscloud/coreai 0.1.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 (216) hide show
  1. package/.prettierrc +9 -0
  2. package/AGENT_SPEC.md +347 -0
  3. package/ARCHITECTURE.md +547 -0
  4. package/DRAFT_PRD.md +1440 -0
  5. package/IMPLEMENTATION_PLAN.md +256 -0
  6. package/PRODUCT.md +473 -0
  7. package/README.md +303 -0
  8. package/WORKFLOWS.md +295 -0
  9. package/agents/_templates/ic-engineer.md +185 -0
  10. package/agents/_templates/reviewer.md +182 -0
  11. package/agents/backend-engineer.yaml +72 -0
  12. package/agents/devops-engineer.yaml +72 -0
  13. package/agents/engineering-manager.yaml +70 -0
  14. package/agents/examples/android-engineer.md +302 -0
  15. package/agents/examples/backend-engineer.md +320 -0
  16. package/agents/examples/devops-engineer.md +742 -0
  17. package/agents/examples/engineering-manager.md +469 -0
  18. package/agents/examples/frontend-engineer.md +58 -0
  19. package/agents/examples/product-manager.md +315 -0
  20. package/agents/examples/qa-engineer.md +371 -0
  21. package/agents/examples/security-engineer.md +525 -0
  22. package/agents/examples/solutions-architect.md +351 -0
  23. package/agents/examples/wearos-engineer.md +359 -0
  24. package/agents/frontend-engineer.yaml +72 -0
  25. package/commands/core/check-inbox.md +34 -0
  26. package/commands/core/delegate.md +30 -0
  27. package/commands/core/git-commit.md +144 -0
  28. package/commands/core/pr-create.md +193 -0
  29. package/commands/core/review.md +56 -0
  30. package/commands/core/sprint-status.md +65 -0
  31. package/commands/optional/docs-update.md +200 -0
  32. package/commands/optional/jira-create.md +200 -0
  33. package/commands/optional/jira-transition.md +184 -0
  34. package/commands/optional/worktree-cleanup.md +167 -0
  35. package/commands/optional/worktree-setup.md +110 -0
  36. package/dist/cli/index.js +4037 -0
  37. package/dist/cli/index.js.map +1 -0
  38. package/dist/index.d.ts +2978 -0
  39. package/dist/index.js +3867 -0
  40. package/dist/index.js.map +1 -0
  41. package/eslint.config.js +29 -0
  42. package/jest.config.js +22 -0
  43. package/knowledge-library/README.md +118 -0
  44. package/knowledge-library/android-engineer/context/current.txt +42 -0
  45. package/knowledge-library/android-engineer/control/decisions.txt +9 -0
  46. package/knowledge-library/android-engineer/control/dependencies.txt +19 -0
  47. package/knowledge-library/android-engineer/control/objectives.txt +26 -0
  48. package/knowledge-library/android-engineer/history/.gitkeep +0 -0
  49. package/knowledge-library/android-engineer/inbox/processed/.gitkeep +0 -0
  50. package/knowledge-library/android-engineer/outbox/.gitkeep +0 -0
  51. package/knowledge-library/android-engineer/tech/.gitkeep +0 -0
  52. package/knowledge-library/architecture.txt +61 -0
  53. package/knowledge-library/backend-engineer/context/current.txt +42 -0
  54. package/knowledge-library/backend-engineer/control/decisions.txt +9 -0
  55. package/knowledge-library/backend-engineer/control/dependencies.txt +19 -0
  56. package/knowledge-library/backend-engineer/control/objectives.txt +26 -0
  57. package/knowledge-library/backend-engineer/history/.gitkeep +0 -0
  58. package/knowledge-library/backend-engineer/inbox/processed/.gitkeep +0 -0
  59. package/knowledge-library/backend-engineer/outbox/.gitkeep +0 -0
  60. package/knowledge-library/backend-engineer/tech/.gitkeep +0 -0
  61. package/knowledge-library/context.txt +52 -0
  62. package/knowledge-library/devops-engineer/context/current.txt +42 -0
  63. package/knowledge-library/devops-engineer/control/decisions.txt +9 -0
  64. package/knowledge-library/devops-engineer/control/dependencies.txt +19 -0
  65. package/knowledge-library/devops-engineer/control/objectives.txt +26 -0
  66. package/knowledge-library/devops-engineer/history/.gitkeep +0 -0
  67. package/knowledge-library/devops-engineer/inbox/processed/.gitkeep +0 -0
  68. package/knowledge-library/devops-engineer/outbox/.gitkeep +0 -0
  69. package/knowledge-library/devops-engineer/tech/.gitkeep +0 -0
  70. package/knowledge-library/engineering-manager/context/current.txt +40 -0
  71. package/knowledge-library/engineering-manager/control/decisions.txt +9 -0
  72. package/knowledge-library/engineering-manager/control/objectives.txt +27 -0
  73. package/knowledge-library/engineering-manager/history/.gitkeep +0 -0
  74. package/knowledge-library/engineering-manager/inbox/processed/.gitkeep +0 -0
  75. package/knowledge-library/engineering-manager/outbox/.gitkeep +0 -0
  76. package/knowledge-library/engineering-manager/tech/.gitkeep +0 -0
  77. package/knowledge-library/prd.txt +81 -0
  78. package/knowledge-library/product-manager/context/current.txt +42 -0
  79. package/knowledge-library/product-manager/control/decisions.txt +9 -0
  80. package/knowledge-library/product-manager/control/dependencies.txt +19 -0
  81. package/knowledge-library/product-manager/control/objectives.txt +26 -0
  82. package/knowledge-library/product-manager/history/.gitkeep +0 -0
  83. package/knowledge-library/product-manager/inbox/processed/.gitkeep +0 -0
  84. package/knowledge-library/product-manager/outbox/.gitkeep +0 -0
  85. package/knowledge-library/product-manager/tech/.gitkeep +0 -0
  86. package/knowledge-library/qa-engineer/context/current.txt +42 -0
  87. package/knowledge-library/qa-engineer/control/decisions.txt +9 -0
  88. package/knowledge-library/qa-engineer/control/dependencies.txt +19 -0
  89. package/knowledge-library/qa-engineer/control/objectives.txt +26 -0
  90. package/knowledge-library/qa-engineer/history/.gitkeep +0 -0
  91. package/knowledge-library/qa-engineer/inbox/processed/.gitkeep +0 -0
  92. package/knowledge-library/qa-engineer/outbox/.gitkeep +0 -0
  93. package/knowledge-library/qa-engineer/tech/.gitkeep +0 -0
  94. package/knowledge-library/security-engineer/context/current.txt +42 -0
  95. package/knowledge-library/security-engineer/control/decisions.txt +9 -0
  96. package/knowledge-library/security-engineer/control/dependencies.txt +19 -0
  97. package/knowledge-library/security-engineer/control/objectives.txt +26 -0
  98. package/knowledge-library/security-engineer/history/.gitkeep +0 -0
  99. package/knowledge-library/security-engineer/inbox/processed/.gitkeep +0 -0
  100. package/knowledge-library/security-engineer/outbox/.gitkeep +0 -0
  101. package/knowledge-library/security-engineer/tech/.gitkeep +0 -0
  102. package/knowledge-library/solutions-architect/context/current.txt +42 -0
  103. package/knowledge-library/solutions-architect/control/decisions.txt +9 -0
  104. package/knowledge-library/solutions-architect/control/dependencies.txt +19 -0
  105. package/knowledge-library/solutions-architect/control/objectives.txt +26 -0
  106. package/knowledge-library/solutions-architect/history/.gitkeep +0 -0
  107. package/knowledge-library/solutions-architect/inbox/processed/.gitkeep +0 -0
  108. package/knowledge-library/solutions-architect/outbox/.gitkeep +0 -0
  109. package/knowledge-library/solutions-architect/tech/.gitkeep +0 -0
  110. package/knowledge-library/wearos-engineer/context/current.txt +42 -0
  111. package/knowledge-library/wearos-engineer/control/decisions.txt +9 -0
  112. package/knowledge-library/wearos-engineer/control/dependencies.txt +19 -0
  113. package/knowledge-library/wearos-engineer/control/objectives.txt +26 -0
  114. package/knowledge-library/wearos-engineer/history/.gitkeep +0 -0
  115. package/knowledge-library/wearos-engineer/inbox/processed/.gitkeep +0 -0
  116. package/knowledge-library/wearos-engineer/outbox/.gitkeep +0 -0
  117. package/knowledge-library/wearos-engineer/tech/.gitkeep +0 -0
  118. package/package.json +66 -0
  119. package/schemas/agent.schema.json +171 -0
  120. package/schemas/coreai.config.schema.json +257 -0
  121. package/scripts/add-agent.sh +323 -0
  122. package/scripts/install.sh +354 -0
  123. package/src/adapters/factory.test.ts +386 -0
  124. package/src/adapters/factory.ts +305 -0
  125. package/src/adapters/index.ts +113 -0
  126. package/src/adapters/interfaces.ts +268 -0
  127. package/src/adapters/mcp/client.test.ts +130 -0
  128. package/src/adapters/mcp/client.ts +451 -0
  129. package/src/adapters/mcp/discovery.test.ts +315 -0
  130. package/src/adapters/mcp/discovery.ts +340 -0
  131. package/src/adapters/mcp/index.ts +66 -0
  132. package/src/adapters/mcp/mapper.test.ts +218 -0
  133. package/src/adapters/mcp/mapper.ts +536 -0
  134. package/src/adapters/mcp/registry.test.ts +433 -0
  135. package/src/adapters/mcp/registry.ts +550 -0
  136. package/src/adapters/mcp/types.ts +258 -0
  137. package/src/adapters/native/filesystem.test.ts +350 -0
  138. package/src/adapters/native/filesystem.ts +393 -0
  139. package/src/adapters/native/github.test.ts +173 -0
  140. package/src/adapters/native/github.ts +627 -0
  141. package/src/adapters/native/index.ts +22 -0
  142. package/src/adapters/native/selector.test.ts +224 -0
  143. package/src/adapters/native/selector.ts +150 -0
  144. package/src/adapters/types.ts +270 -0
  145. package/src/agents/compiler.test.ts +399 -0
  146. package/src/agents/compiler.ts +359 -0
  147. package/src/agents/index.ts +36 -0
  148. package/src/agents/loader.test.ts +319 -0
  149. package/src/agents/loader.ts +143 -0
  150. package/src/agents/resolver.test.ts +282 -0
  151. package/src/agents/resolver.ts +262 -0
  152. package/src/agents/types.ts +87 -0
  153. package/src/cache/index.ts +38 -0
  154. package/src/cache/interfaces.ts +283 -0
  155. package/src/cache/manager.test.ts +266 -0
  156. package/src/cache/manager.ts +388 -0
  157. package/src/cache/provider.test.ts +485 -0
  158. package/src/cache/provider.ts +745 -0
  159. package/src/cache/types.test.ts +192 -0
  160. package/src/cache/types.ts +313 -0
  161. package/src/cli/commands/build.test.ts +248 -0
  162. package/src/cli/commands/build.ts +244 -0
  163. package/src/cli/commands/cache.test.ts +221 -0
  164. package/src/cli/commands/cache.ts +229 -0
  165. package/src/cli/commands/index.ts +63 -0
  166. package/src/cli/commands/init.test.ts +173 -0
  167. package/src/cli/commands/init.ts +296 -0
  168. package/src/cli/commands/skills.test.ts +272 -0
  169. package/src/cli/commands/skills.ts +348 -0
  170. package/src/cli/commands/status.test.ts +392 -0
  171. package/src/cli/commands/status.ts +332 -0
  172. package/src/cli/commands/sync.test.ts +213 -0
  173. package/src/cli/commands/sync.ts +251 -0
  174. package/src/cli/commands/validate.test.ts +216 -0
  175. package/src/cli/commands/validate.ts +340 -0
  176. package/src/cli/index.test.ts +190 -0
  177. package/src/cli/index.ts +493 -0
  178. package/src/commands/context.test.ts +163 -0
  179. package/src/commands/context.ts +111 -0
  180. package/src/commands/index.ts +56 -0
  181. package/src/commands/loader.test.ts +273 -0
  182. package/src/commands/loader.ts +355 -0
  183. package/src/commands/registry.test.ts +384 -0
  184. package/src/commands/registry.ts +248 -0
  185. package/src/commands/runner.test.ts +297 -0
  186. package/src/commands/runner.ts +222 -0
  187. package/src/commands/types.ts +361 -0
  188. package/src/config/index.ts +19 -0
  189. package/src/config/loader.test.ts +262 -0
  190. package/src/config/loader.ts +188 -0
  191. package/src/config/types.ts +154 -0
  192. package/src/context/index.ts +14 -0
  193. package/src/context/loader.test.ts +334 -0
  194. package/src/context/loader.ts +357 -0
  195. package/src/index.test.ts +13 -0
  196. package/src/index.ts +244 -0
  197. package/src/knowledge-library/index.ts +44 -0
  198. package/src/knowledge-library/manager.test.ts +536 -0
  199. package/src/knowledge-library/manager.ts +804 -0
  200. package/src/knowledge-library/types.ts +432 -0
  201. package/src/skills/generator.test.ts +602 -0
  202. package/src/skills/generator.ts +491 -0
  203. package/src/skills/index.ts +27 -0
  204. package/src/skills/templates.ts +520 -0
  205. package/src/skills/types.ts +251 -0
  206. package/templates/completion-report.md +72 -0
  207. package/templates/feedback.md +56 -0
  208. package/templates/project-files/CLAUDE.md.template +109 -0
  209. package/templates/project-files/coreai.json.example +47 -0
  210. package/templates/project-files/mcp.json.template +20 -0
  211. package/templates/review-complete.md +64 -0
  212. package/templates/review-request.md +67 -0
  213. package/templates/task-assignment.md +51 -0
  214. package/tsconfig.build.json +4 -0
  215. package/tsconfig.json +26 -0
  216. package/tsup.config.ts +23 -0
@@ -0,0 +1,87 @@
1
+ /**
2
+ * CoreAI Agent Types
3
+ *
4
+ * TypeScript types corresponding to the JSON schema at schemas/agent.schema.json
5
+ */
6
+
7
+ export type AgentType = 'ic-engineer' | 'manager' | 'specialist' | 'coordinator';
8
+
9
+ export type WorkflowType =
10
+ | 'ticket-implementation'
11
+ | 'bug-investigation'
12
+ | 'code-review'
13
+ | 'planning-estimation';
14
+
15
+ export interface AgentExpertise {
16
+ primary?: string[];
17
+ tech_stack?: string;
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ export interface AgentPrinciples {
22
+ code_quality?: string[];
23
+ testing?: string[];
24
+ security?: string[];
25
+ performance?: string[];
26
+ [key: string]: string[] | undefined;
27
+ }
28
+
29
+ export interface AgentBehaviors {
30
+ workflow?: WorkflowType;
31
+ quality_gates?: string;
32
+ [key: string]: unknown;
33
+ }
34
+
35
+ export interface AgentContextSources {
36
+ shared?: string[];
37
+ personal?: string[];
38
+ }
39
+
40
+ export interface AgentCommunication {
41
+ inbox?: string;
42
+ outbox?: string;
43
+ }
44
+
45
+ /**
46
+ * Raw agent definition as loaded from YAML (before variable resolution)
47
+ */
48
+ export interface AgentDefinition {
49
+ role: string;
50
+ type: AgentType;
51
+ display_name: string;
52
+ description: string;
53
+ responsibilities?: string[];
54
+ expertise?: AgentExpertise;
55
+ skills?: string[];
56
+ principles?: AgentPrinciples;
57
+ behaviors?: AgentBehaviors;
58
+ context_sources?: AgentContextSources;
59
+ communication?: AgentCommunication;
60
+ }
61
+
62
+ /**
63
+ * Agent definition after variable resolution
64
+ */
65
+ export interface ResolvedAgentDefinition extends AgentDefinition {
66
+ // After resolution, these fields may have expanded values
67
+ expertise?: AgentExpertise & {
68
+ tech_stack?: Record<string, unknown>;
69
+ };
70
+ behaviors?: AgentBehaviors & {
71
+ quality_gates?: Record<string, unknown>;
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Agent source location
77
+ */
78
+ export type AgentSource = 'core' | 'custom' | 'override';
79
+
80
+ /**
81
+ * Agent metadata including source information
82
+ */
83
+ export interface AgentMetadata {
84
+ definition: AgentDefinition;
85
+ source: AgentSource;
86
+ filePath: string;
87
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Cache Module
3
+ *
4
+ * Provides the shared context cache system for storing and retrieving
5
+ * content from remote documentation providers.
6
+ */
7
+
8
+ // Types
9
+ export type {
10
+ CacheSource,
11
+ CacheStatus,
12
+ CacheMetadata,
13
+ CacheEntry,
14
+ CacheOptions,
15
+ CacheListOptions,
16
+ CacheStats,
17
+ SyncResult,
18
+ CacheErrorCode,
19
+ } from './types.js';
20
+
21
+ export { CacheError, CACHE_PATHS, DEFAULT_CACHE_CONFIG } from './types.js';
22
+
23
+ // Interfaces
24
+ export type {
25
+ CacheProvider,
26
+ RemoteFetcher,
27
+ FetchOptions,
28
+ CacheManager,
29
+ SyncOptions,
30
+ } from './interfaces.js';
31
+
32
+ // Provider
33
+ export type { FileCacheProviderOptions } from './provider.js';
34
+ export { FileCacheProvider, createFileCacheProvider } from './provider.js';
35
+
36
+ // Manager
37
+ export type { CacheManagerOptions } from './manager.js';
38
+ export { CacheManager as FileCacheManager, createCacheManager } from './manager.js';
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Cache Interfaces
3
+ *
4
+ * Interface definitions for the cache system.
5
+ */
6
+
7
+ import type {
8
+ CacheEntry,
9
+ CacheMetadata,
10
+ CacheOptions,
11
+ CacheListOptions,
12
+ CacheStats,
13
+ CacheStatus,
14
+ SyncResult,
15
+ } from './types.js';
16
+
17
+ /**
18
+ * Cache provider interface
19
+ *
20
+ * Defines the contract for cache implementations.
21
+ */
22
+ export interface CacheProvider {
23
+ /**
24
+ * Initialize the cache (create directories, load index)
25
+ */
26
+ initialize(): Promise<void>;
27
+
28
+ /**
29
+ * Check if the cache is initialized
30
+ */
31
+ isInitialized(): boolean;
32
+
33
+ /**
34
+ * Get a cached entry by key
35
+ *
36
+ * @param key - Cache key
37
+ * @param options - Optional cache options
38
+ * @returns The cached entry or null if not found/expired
39
+ */
40
+ get<T = string>(key: string, options?: CacheOptions): Promise<CacheEntry<T> | null>;
41
+
42
+ /**
43
+ * Get the content only (convenience method)
44
+ *
45
+ * @param key - Cache key
46
+ * @param options - Optional cache options
47
+ * @returns The cached content or null if not found/expired
48
+ */
49
+ getContent<T = string>(key: string, options?: CacheOptions): Promise<T | null>;
50
+
51
+ /**
52
+ * Set a cache entry
53
+ *
54
+ * @param key - Cache key
55
+ * @param content - Content to cache
56
+ * @param metadata - Partial metadata (key and cachedAt will be set automatically)
57
+ * @param options - Optional cache options
58
+ */
59
+ set<T = string>(
60
+ key: string,
61
+ content: T,
62
+ metadata: Partial<CacheMetadata>,
63
+ options?: CacheOptions
64
+ ): Promise<void>;
65
+
66
+ /**
67
+ * Check if a key exists in the cache
68
+ *
69
+ * @param key - Cache key
70
+ * @returns True if the key exists (regardless of expiration)
71
+ */
72
+ has(key: string): Promise<boolean>;
73
+
74
+ /**
75
+ * Delete a cache entry
76
+ *
77
+ * @param key - Cache key
78
+ * @returns True if the entry was deleted
79
+ */
80
+ delete(key: string): Promise<boolean>;
81
+
82
+ /**
83
+ * Get the status of a cache entry
84
+ *
85
+ * @param key - Cache key
86
+ * @returns The cache status or null if not found
87
+ */
88
+ getStatus(key: string): Promise<CacheStatus | null>;
89
+
90
+ /**
91
+ * Get metadata for a cache entry
92
+ *
93
+ * @param key - Cache key
94
+ * @returns The metadata or null if not found
95
+ */
96
+ getMetadata(key: string): Promise<CacheMetadata | null>;
97
+
98
+ /**
99
+ * Update metadata for a cache entry
100
+ *
101
+ * @param key - Cache key
102
+ * @param metadata - Partial metadata to update
103
+ */
104
+ updateMetadata(key: string, metadata: Partial<CacheMetadata>): Promise<void>;
105
+
106
+ /**
107
+ * List cache entries
108
+ *
109
+ * @param options - List filter options
110
+ * @returns Array of metadata entries
111
+ */
112
+ list(options?: CacheListOptions): Promise<CacheMetadata[]>;
113
+
114
+ /**
115
+ * Get cache statistics
116
+ */
117
+ getStats(): Promise<CacheStats>;
118
+
119
+ /**
120
+ * Clear all cache entries
121
+ *
122
+ * @returns Number of entries cleared
123
+ */
124
+ clear(): Promise<number>;
125
+
126
+ /**
127
+ * Clear expired entries only
128
+ *
129
+ * @returns Number of entries cleared
130
+ */
131
+ clearExpired(): Promise<number>;
132
+
133
+ /**
134
+ * Clear entries by source
135
+ *
136
+ * @param source - Source to clear
137
+ * @returns Number of entries cleared
138
+ */
139
+ clearBySource(source: string): Promise<number>;
140
+
141
+ /**
142
+ * Clear entries by tag
143
+ *
144
+ * @param tag - Tag to clear
145
+ * @returns Number of entries cleared
146
+ */
147
+ clearByTag(tag: string): Promise<number>;
148
+ }
149
+
150
+ /**
151
+ * Remote fetcher interface
152
+ *
153
+ * Used by the cache to fetch content from remote sources.
154
+ */
155
+ export interface RemoteFetcher {
156
+ /**
157
+ * Fetch content from a remote source
158
+ *
159
+ * @param url - Source URL or identifier
160
+ * @param options - Fetch options
161
+ * @returns The fetched content and metadata
162
+ */
163
+ fetch(
164
+ url: string,
165
+ options?: FetchOptions
166
+ ): Promise<{ content: string; metadata: Partial<CacheMetadata> }>;
167
+
168
+ /**
169
+ * Check if content has changed (conditional fetch)
170
+ *
171
+ * @param url - Source URL or identifier
172
+ * @param etag - Previous ETag
173
+ * @returns True if content has changed
174
+ */
175
+ hasChanged(url: string, etag?: string): Promise<boolean>;
176
+ }
177
+
178
+ /**
179
+ * Options for remote fetching
180
+ */
181
+ export interface FetchOptions {
182
+ /**
183
+ * Request timeout in milliseconds
184
+ */
185
+ timeout?: number;
186
+
187
+ /**
188
+ * ETag for conditional fetching
189
+ */
190
+ etag?: string;
191
+
192
+ /**
193
+ * Custom headers
194
+ */
195
+ headers?: Record<string, string>;
196
+ }
197
+
198
+ /**
199
+ * Cache manager interface
200
+ *
201
+ * Higher-level interface that combines caching with remote fetching.
202
+ */
203
+ export interface CacheManager {
204
+ /**
205
+ * Get content with cache-first strategy
206
+ *
207
+ * 1. Check cache
208
+ * 2. If cached and valid, return cached content
209
+ * 3. If stale or missing, fetch from remote
210
+ * 4. Cache the fetched content
211
+ * 5. Return the content
212
+ *
213
+ * @param key - Cache key
214
+ * @param url - Remote URL to fetch if not cached
215
+ * @param options - Cache and fetch options
216
+ */
217
+ getWithFallback<T = string>(
218
+ key: string,
219
+ url: string,
220
+ options?: CacheOptions & FetchOptions
221
+ ): Promise<CacheEntry<T>>;
222
+
223
+ /**
224
+ * Sync all entries from a source
225
+ *
226
+ * @param source - Source to sync
227
+ * @param options - Sync options
228
+ */
229
+ syncSource(source: string, options?: SyncOptions): Promise<SyncResult>;
230
+
231
+ /**
232
+ * Sync specific entries
233
+ *
234
+ * @param keys - Keys to sync
235
+ * @param options - Sync options
236
+ */
237
+ syncEntries(keys: string[], options?: SyncOptions): Promise<SyncResult>;
238
+
239
+ /**
240
+ * Register a fetcher for a source
241
+ *
242
+ * @param source - Source identifier
243
+ * @param fetcher - Fetcher implementation
244
+ */
245
+ registerFetcher(source: string, fetcher: RemoteFetcher): void;
246
+
247
+ /**
248
+ * Check if a fetcher is registered for a source
249
+ *
250
+ * @param source - Source identifier
251
+ */
252
+ hasFetcher(source: string): boolean;
253
+
254
+ /**
255
+ * Get the underlying cache provider
256
+ */
257
+ getCache(): CacheProvider;
258
+ }
259
+
260
+ /**
261
+ * Options for sync operations
262
+ */
263
+ export interface SyncOptions {
264
+ /**
265
+ * Force refresh all entries
266
+ */
267
+ force?: boolean;
268
+
269
+ /**
270
+ * Continue on error
271
+ */
272
+ continueOnError?: boolean;
273
+
274
+ /**
275
+ * Maximum concurrent fetches
276
+ */
277
+ concurrency?: number;
278
+
279
+ /**
280
+ * Progress callback
281
+ */
282
+ onProgress?: (completed: number, total: number) => void;
283
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Cache Manager Tests
3
+ */
4
+
5
+ import { promises as fs } from 'fs';
6
+ import { join } from 'path';
7
+ import { tmpdir } from 'os';
8
+ import { CacheManager, createCacheManager } from './manager.js';
9
+ import { FileCacheProvider } from './provider.js';
10
+ import { CacheError } from './types.js';
11
+ import type { RemoteFetcher } from './index.js';
12
+
13
+ describe('CacheManager', () => {
14
+ let testDir: string;
15
+ let cacheProvider: FileCacheProvider;
16
+ let manager: CacheManager;
17
+
18
+ // Mock fetcher for testing
19
+ const createMockFetcher = (responses: Record<string, string>): RemoteFetcher => ({
20
+ async fetch(url: string) {
21
+ const content = responses[url];
22
+ if (!content) {
23
+ throw new Error(`Not found: ${url}`);
24
+ }
25
+ return {
26
+ content,
27
+ metadata: {
28
+ contentType: 'text/plain',
29
+ etag: `etag-${url}`,
30
+ title: `Title for ${url}`,
31
+ },
32
+ };
33
+ },
34
+ async hasChanged(_url: string, _etag?: string) {
35
+ return true; // Always changed for testing
36
+ },
37
+ });
38
+
39
+ beforeEach(async () => {
40
+ testDir = join(tmpdir(), `manager-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
41
+ await fs.mkdir(testDir, { recursive: true });
42
+
43
+ cacheProvider = new FileCacheProvider({ basePath: testDir });
44
+ await cacheProvider.initialize();
45
+
46
+ manager = new CacheManager({
47
+ cache: cacheProvider,
48
+ defaultTtl: 3600,
49
+ });
50
+ });
51
+
52
+ afterEach(async () => {
53
+ try {
54
+ await fs.rm(testDir, { recursive: true, force: true });
55
+ } catch {
56
+ // Ignore cleanup errors
57
+ }
58
+ });
59
+
60
+ describe('constructor', () => {
61
+ it('should create manager with options', () => {
62
+ expect(manager).toBeInstanceOf(CacheManager);
63
+ });
64
+
65
+ it('should use default values', () => {
66
+ const m = new CacheManager({ cache: cacheProvider });
67
+ expect(m).toBeInstanceOf(CacheManager);
68
+ });
69
+ });
70
+
71
+ describe('registerFetcher', () => {
72
+ it('should register a fetcher', () => {
73
+ const fetcher = createMockFetcher({});
74
+ manager.registerFetcher('github', fetcher);
75
+ expect(manager.hasFetcher('github')).toBe(true);
76
+ });
77
+
78
+ it('should track registered sources', () => {
79
+ manager.registerFetcher('github', createMockFetcher({}));
80
+ manager.registerFetcher('confluence', createMockFetcher({}));
81
+
82
+ const sources = manager.getRegisteredSources();
83
+ expect(sources).toContain('github');
84
+ expect(sources).toContain('confluence');
85
+ });
86
+ });
87
+
88
+ describe('getWithFallback', () => {
89
+ it('should fetch and cache content', async () => {
90
+ const fetcher = createMockFetcher({
91
+ 'https://github.com/test': 'GitHub content',
92
+ });
93
+ manager.registerFetcher('github', fetcher);
94
+
95
+ const entry = await manager.getWithFallback('test-key', 'https://github.com/test');
96
+
97
+ expect(entry.content).toBe('GitHub content');
98
+ expect(entry.metadata.source).toBe('github');
99
+ });
100
+
101
+ it('should return cached content on second call', async () => {
102
+ const responses = { 'https://github.com/test': 'Original content' };
103
+ const fetcher = createMockFetcher(responses);
104
+ manager.registerFetcher('github', fetcher);
105
+
106
+ // First call - fetches
107
+ await manager.getWithFallback('cached-key', 'https://github.com/test');
108
+
109
+ // Change the response
110
+ responses['https://github.com/test'] = 'Updated content';
111
+
112
+ // Second call - should return cached
113
+ const entry = await manager.getWithFallback('cached-key', 'https://github.com/test');
114
+ expect(entry.content).toBe('Original content');
115
+ });
116
+
117
+ it('should force refresh when option set', async () => {
118
+ const responses = { 'https://github.com/test': 'Original content' };
119
+ const fetcher = createMockFetcher(responses);
120
+ manager.registerFetcher('github', fetcher);
121
+
122
+ await manager.getWithFallback('force-key', 'https://github.com/test');
123
+
124
+ responses['https://github.com/test'] = 'Updated content';
125
+
126
+ const entry = await manager.getWithFallback('force-key', 'https://github.com/test', {
127
+ forceRefresh: true,
128
+ });
129
+ expect(entry.content).toBe('Updated content');
130
+ });
131
+
132
+ it('should throw if no fetcher registered', async () => {
133
+ await expect(manager.getWithFallback('key', 'https://unknown.com/test')).rejects.toThrow(
134
+ CacheError
135
+ );
136
+ });
137
+
138
+ it('should skip cache when option set', async () => {
139
+ const responses = { 'https://github.com/test': 'Content' };
140
+ const fetcher = createMockFetcher(responses);
141
+ manager.registerFetcher('github', fetcher);
142
+
143
+ // Cache something first
144
+ await manager.getWithFallback('skip-key', 'https://github.com/test');
145
+
146
+ responses['https://github.com/test'] = 'New content';
147
+
148
+ // Skip cache should fetch new content
149
+ const entry = await manager.getWithFallback('skip-key', 'https://github.com/test', {
150
+ skipCache: true,
151
+ });
152
+ expect(entry.content).toBe('New content');
153
+ });
154
+
155
+ it('should apply tags to cached entry', async () => {
156
+ const fetcher = createMockFetcher({
157
+ 'https://github.com/test': 'Content',
158
+ });
159
+ manager.registerFetcher('github', fetcher);
160
+
161
+ await manager.getWithFallback('tagged-key', 'https://github.com/test', {
162
+ tags: ['docs', 'api'],
163
+ });
164
+
165
+ const metadata = await cacheProvider.getMetadata('tagged-key');
166
+ expect(metadata?.tags).toEqual(['docs', 'api']);
167
+ });
168
+ });
169
+
170
+ describe('getCache', () => {
171
+ it('should return the underlying cache provider', () => {
172
+ const cache = manager.getCache();
173
+ expect(cache).toBe(cacheProvider);
174
+ });
175
+ });
176
+
177
+ describe('hasFetcher', () => {
178
+ it('should return false for unregistered source', () => {
179
+ expect(manager.hasFetcher('unknown')).toBe(false);
180
+ });
181
+
182
+ it('should return true for registered source', () => {
183
+ manager.registerFetcher('test', createMockFetcher({}));
184
+ expect(manager.hasFetcher('test')).toBe(true);
185
+ });
186
+ });
187
+
188
+ describe('source detection', () => {
189
+ it('should detect GitHub URLs', async () => {
190
+ const fetcher = createMockFetcher({
191
+ 'https://github.com/owner/repo': 'Content',
192
+ });
193
+ manager.registerFetcher('github', fetcher);
194
+
195
+ const entry = await manager.getWithFallback('github-key', 'https://github.com/owner/repo');
196
+ expect(entry.metadata.source).toBe('github');
197
+ });
198
+
199
+ it('should detect Confluence URLs', async () => {
200
+ const fetcher = createMockFetcher({
201
+ 'https://mycompany.atlassian.net/wiki/page': 'Content',
202
+ });
203
+ manager.registerFetcher('confluence', fetcher);
204
+
205
+ const entry = await manager.getWithFallback(
206
+ 'confluence-key',
207
+ 'https://mycompany.atlassian.net/wiki/page'
208
+ );
209
+ expect(entry.metadata.source).toBe('confluence');
210
+ });
211
+
212
+ it('should detect Notion URLs', async () => {
213
+ const fetcher = createMockFetcher({
214
+ 'https://notion.so/page': 'Content',
215
+ });
216
+ manager.registerFetcher('notion', fetcher);
217
+
218
+ const entry = await manager.getWithFallback('notion-key', 'https://notion.so/page');
219
+ expect(entry.metadata.source).toBe('notion');
220
+ });
221
+ });
222
+
223
+ describe('syncEntries', () => {
224
+ beforeEach(async () => {
225
+ const fetcher = createMockFetcher({
226
+ 'https://github.com/test1': 'Content 1',
227
+ 'https://github.com/test2': 'Content 2',
228
+ });
229
+ manager.registerFetcher('github', fetcher);
230
+
231
+ // Pre-populate cache
232
+ await manager.getWithFallback('key1', 'https://github.com/test1');
233
+ await manager.getWithFallback('key2', 'https://github.com/test2');
234
+ });
235
+
236
+ it('should sync specified entries', async () => {
237
+ const result = await manager.syncEntries(['key1', 'key2']);
238
+
239
+ expect(result.updated).toBeGreaterThanOrEqual(0);
240
+ expect(result.failed).toBe(0);
241
+ expect(result.duration).toBeGreaterThanOrEqual(0);
242
+ });
243
+
244
+ it('should report errors for missing entries', async () => {
245
+ const result = await manager.syncEntries(['missing-key'], { continueOnError: true });
246
+
247
+ expect(result.failed).toBe(1);
248
+ expect(result.errors[0].key).toBe('missing-key');
249
+ });
250
+ });
251
+ });
252
+
253
+ describe('createCacheManager', () => {
254
+ it('should create a manager instance', async () => {
255
+ const testDir = join(tmpdir(), `create-manager-${Date.now()}`);
256
+ await fs.mkdir(testDir, { recursive: true });
257
+
258
+ const cache = new FileCacheProvider({ basePath: testDir });
259
+ await cache.initialize();
260
+
261
+ const manager = createCacheManager({ cache });
262
+ expect(manager).toBeInstanceOf(CacheManager);
263
+
264
+ await fs.rm(testDir, { recursive: true, force: true });
265
+ });
266
+ });