@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.
- package/.prettierrc +9 -0
- package/AGENT_SPEC.md +347 -0
- package/ARCHITECTURE.md +547 -0
- package/DRAFT_PRD.md +1440 -0
- package/IMPLEMENTATION_PLAN.md +256 -0
- package/PRODUCT.md +473 -0
- package/README.md +303 -0
- package/WORKFLOWS.md +295 -0
- package/agents/_templates/ic-engineer.md +185 -0
- package/agents/_templates/reviewer.md +182 -0
- package/agents/backend-engineer.yaml +72 -0
- package/agents/devops-engineer.yaml +72 -0
- package/agents/engineering-manager.yaml +70 -0
- package/agents/examples/android-engineer.md +302 -0
- package/agents/examples/backend-engineer.md +320 -0
- package/agents/examples/devops-engineer.md +742 -0
- package/agents/examples/engineering-manager.md +469 -0
- package/agents/examples/frontend-engineer.md +58 -0
- package/agents/examples/product-manager.md +315 -0
- package/agents/examples/qa-engineer.md +371 -0
- package/agents/examples/security-engineer.md +525 -0
- package/agents/examples/solutions-architect.md +351 -0
- package/agents/examples/wearos-engineer.md +359 -0
- package/agents/frontend-engineer.yaml +72 -0
- package/commands/core/check-inbox.md +34 -0
- package/commands/core/delegate.md +30 -0
- package/commands/core/git-commit.md +144 -0
- package/commands/core/pr-create.md +193 -0
- package/commands/core/review.md +56 -0
- package/commands/core/sprint-status.md +65 -0
- package/commands/optional/docs-update.md +200 -0
- package/commands/optional/jira-create.md +200 -0
- package/commands/optional/jira-transition.md +184 -0
- package/commands/optional/worktree-cleanup.md +167 -0
- package/commands/optional/worktree-setup.md +110 -0
- package/dist/cli/index.js +4037 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +2978 -0
- package/dist/index.js +3867 -0
- package/dist/index.js.map +1 -0
- package/eslint.config.js +29 -0
- package/jest.config.js +22 -0
- package/knowledge-library/README.md +118 -0
- package/knowledge-library/android-engineer/context/current.txt +42 -0
- package/knowledge-library/android-engineer/control/decisions.txt +9 -0
- package/knowledge-library/android-engineer/control/dependencies.txt +19 -0
- package/knowledge-library/android-engineer/control/objectives.txt +26 -0
- package/knowledge-library/android-engineer/history/.gitkeep +0 -0
- package/knowledge-library/android-engineer/inbox/processed/.gitkeep +0 -0
- package/knowledge-library/android-engineer/outbox/.gitkeep +0 -0
- package/knowledge-library/android-engineer/tech/.gitkeep +0 -0
- package/knowledge-library/architecture.txt +61 -0
- package/knowledge-library/backend-engineer/context/current.txt +42 -0
- package/knowledge-library/backend-engineer/control/decisions.txt +9 -0
- package/knowledge-library/backend-engineer/control/dependencies.txt +19 -0
- package/knowledge-library/backend-engineer/control/objectives.txt +26 -0
- package/knowledge-library/backend-engineer/history/.gitkeep +0 -0
- package/knowledge-library/backend-engineer/inbox/processed/.gitkeep +0 -0
- package/knowledge-library/backend-engineer/outbox/.gitkeep +0 -0
- package/knowledge-library/backend-engineer/tech/.gitkeep +0 -0
- package/knowledge-library/context.txt +52 -0
- package/knowledge-library/devops-engineer/context/current.txt +42 -0
- package/knowledge-library/devops-engineer/control/decisions.txt +9 -0
- package/knowledge-library/devops-engineer/control/dependencies.txt +19 -0
- package/knowledge-library/devops-engineer/control/objectives.txt +26 -0
- package/knowledge-library/devops-engineer/history/.gitkeep +0 -0
- package/knowledge-library/devops-engineer/inbox/processed/.gitkeep +0 -0
- package/knowledge-library/devops-engineer/outbox/.gitkeep +0 -0
- package/knowledge-library/devops-engineer/tech/.gitkeep +0 -0
- package/knowledge-library/engineering-manager/context/current.txt +40 -0
- package/knowledge-library/engineering-manager/control/decisions.txt +9 -0
- package/knowledge-library/engineering-manager/control/objectives.txt +27 -0
- package/knowledge-library/engineering-manager/history/.gitkeep +0 -0
- package/knowledge-library/engineering-manager/inbox/processed/.gitkeep +0 -0
- package/knowledge-library/engineering-manager/outbox/.gitkeep +0 -0
- package/knowledge-library/engineering-manager/tech/.gitkeep +0 -0
- package/knowledge-library/prd.txt +81 -0
- package/knowledge-library/product-manager/context/current.txt +42 -0
- package/knowledge-library/product-manager/control/decisions.txt +9 -0
- package/knowledge-library/product-manager/control/dependencies.txt +19 -0
- package/knowledge-library/product-manager/control/objectives.txt +26 -0
- package/knowledge-library/product-manager/history/.gitkeep +0 -0
- package/knowledge-library/product-manager/inbox/processed/.gitkeep +0 -0
- package/knowledge-library/product-manager/outbox/.gitkeep +0 -0
- package/knowledge-library/product-manager/tech/.gitkeep +0 -0
- package/knowledge-library/qa-engineer/context/current.txt +42 -0
- package/knowledge-library/qa-engineer/control/decisions.txt +9 -0
- package/knowledge-library/qa-engineer/control/dependencies.txt +19 -0
- package/knowledge-library/qa-engineer/control/objectives.txt +26 -0
- package/knowledge-library/qa-engineer/history/.gitkeep +0 -0
- package/knowledge-library/qa-engineer/inbox/processed/.gitkeep +0 -0
- package/knowledge-library/qa-engineer/outbox/.gitkeep +0 -0
- package/knowledge-library/qa-engineer/tech/.gitkeep +0 -0
- package/knowledge-library/security-engineer/context/current.txt +42 -0
- package/knowledge-library/security-engineer/control/decisions.txt +9 -0
- package/knowledge-library/security-engineer/control/dependencies.txt +19 -0
- package/knowledge-library/security-engineer/control/objectives.txt +26 -0
- package/knowledge-library/security-engineer/history/.gitkeep +0 -0
- package/knowledge-library/security-engineer/inbox/processed/.gitkeep +0 -0
- package/knowledge-library/security-engineer/outbox/.gitkeep +0 -0
- package/knowledge-library/security-engineer/tech/.gitkeep +0 -0
- package/knowledge-library/solutions-architect/context/current.txt +42 -0
- package/knowledge-library/solutions-architect/control/decisions.txt +9 -0
- package/knowledge-library/solutions-architect/control/dependencies.txt +19 -0
- package/knowledge-library/solutions-architect/control/objectives.txt +26 -0
- package/knowledge-library/solutions-architect/history/.gitkeep +0 -0
- package/knowledge-library/solutions-architect/inbox/processed/.gitkeep +0 -0
- package/knowledge-library/solutions-architect/outbox/.gitkeep +0 -0
- package/knowledge-library/solutions-architect/tech/.gitkeep +0 -0
- package/knowledge-library/wearos-engineer/context/current.txt +42 -0
- package/knowledge-library/wearos-engineer/control/decisions.txt +9 -0
- package/knowledge-library/wearos-engineer/control/dependencies.txt +19 -0
- package/knowledge-library/wearos-engineer/control/objectives.txt +26 -0
- package/knowledge-library/wearos-engineer/history/.gitkeep +0 -0
- package/knowledge-library/wearos-engineer/inbox/processed/.gitkeep +0 -0
- package/knowledge-library/wearos-engineer/outbox/.gitkeep +0 -0
- package/knowledge-library/wearos-engineer/tech/.gitkeep +0 -0
- package/package.json +66 -0
- package/schemas/agent.schema.json +171 -0
- package/schemas/coreai.config.schema.json +257 -0
- package/scripts/add-agent.sh +323 -0
- package/scripts/install.sh +354 -0
- package/src/adapters/factory.test.ts +386 -0
- package/src/adapters/factory.ts +305 -0
- package/src/adapters/index.ts +113 -0
- package/src/adapters/interfaces.ts +268 -0
- package/src/adapters/mcp/client.test.ts +130 -0
- package/src/adapters/mcp/client.ts +451 -0
- package/src/adapters/mcp/discovery.test.ts +315 -0
- package/src/adapters/mcp/discovery.ts +340 -0
- package/src/adapters/mcp/index.ts +66 -0
- package/src/adapters/mcp/mapper.test.ts +218 -0
- package/src/adapters/mcp/mapper.ts +536 -0
- package/src/adapters/mcp/registry.test.ts +433 -0
- package/src/adapters/mcp/registry.ts +550 -0
- package/src/adapters/mcp/types.ts +258 -0
- package/src/adapters/native/filesystem.test.ts +350 -0
- package/src/adapters/native/filesystem.ts +393 -0
- package/src/adapters/native/github.test.ts +173 -0
- package/src/adapters/native/github.ts +627 -0
- package/src/adapters/native/index.ts +22 -0
- package/src/adapters/native/selector.test.ts +224 -0
- package/src/adapters/native/selector.ts +150 -0
- package/src/adapters/types.ts +270 -0
- package/src/agents/compiler.test.ts +399 -0
- package/src/agents/compiler.ts +359 -0
- package/src/agents/index.ts +36 -0
- package/src/agents/loader.test.ts +319 -0
- package/src/agents/loader.ts +143 -0
- package/src/agents/resolver.test.ts +282 -0
- package/src/agents/resolver.ts +262 -0
- package/src/agents/types.ts +87 -0
- package/src/cache/index.ts +38 -0
- package/src/cache/interfaces.ts +283 -0
- package/src/cache/manager.test.ts +266 -0
- package/src/cache/manager.ts +388 -0
- package/src/cache/provider.test.ts +485 -0
- package/src/cache/provider.ts +745 -0
- package/src/cache/types.test.ts +192 -0
- package/src/cache/types.ts +313 -0
- package/src/cli/commands/build.test.ts +248 -0
- package/src/cli/commands/build.ts +244 -0
- package/src/cli/commands/cache.test.ts +221 -0
- package/src/cli/commands/cache.ts +229 -0
- package/src/cli/commands/index.ts +63 -0
- package/src/cli/commands/init.test.ts +173 -0
- package/src/cli/commands/init.ts +296 -0
- package/src/cli/commands/skills.test.ts +272 -0
- package/src/cli/commands/skills.ts +348 -0
- package/src/cli/commands/status.test.ts +392 -0
- package/src/cli/commands/status.ts +332 -0
- package/src/cli/commands/sync.test.ts +213 -0
- package/src/cli/commands/sync.ts +251 -0
- package/src/cli/commands/validate.test.ts +216 -0
- package/src/cli/commands/validate.ts +340 -0
- package/src/cli/index.test.ts +190 -0
- package/src/cli/index.ts +493 -0
- package/src/commands/context.test.ts +163 -0
- package/src/commands/context.ts +111 -0
- package/src/commands/index.ts +56 -0
- package/src/commands/loader.test.ts +273 -0
- package/src/commands/loader.ts +355 -0
- package/src/commands/registry.test.ts +384 -0
- package/src/commands/registry.ts +248 -0
- package/src/commands/runner.test.ts +297 -0
- package/src/commands/runner.ts +222 -0
- package/src/commands/types.ts +361 -0
- package/src/config/index.ts +19 -0
- package/src/config/loader.test.ts +262 -0
- package/src/config/loader.ts +188 -0
- package/src/config/types.ts +154 -0
- package/src/context/index.ts +14 -0
- package/src/context/loader.test.ts +334 -0
- package/src/context/loader.ts +357 -0
- package/src/index.test.ts +13 -0
- package/src/index.ts +244 -0
- package/src/knowledge-library/index.ts +44 -0
- package/src/knowledge-library/manager.test.ts +536 -0
- package/src/knowledge-library/manager.ts +804 -0
- package/src/knowledge-library/types.ts +432 -0
- package/src/skills/generator.test.ts +602 -0
- package/src/skills/generator.ts +491 -0
- package/src/skills/index.ts +27 -0
- package/src/skills/templates.ts +520 -0
- package/src/skills/types.ts +251 -0
- package/templates/completion-report.md +72 -0
- package/templates/feedback.md +56 -0
- package/templates/project-files/CLAUDE.md.template +109 -0
- package/templates/project-files/coreai.json.example +47 -0
- package/templates/project-files/mcp.json.template +20 -0
- package/templates/review-complete.md +64 -0
- package/templates/review-request.md +67 -0
- package/templates/task-assignment.md +51 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +26 -0
- package/tsup.config.ts +23 -0
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-based Cache Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements CacheProvider using the local filesystem.
|
|
5
|
+
* Stores content in files with separate metadata JSON files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { promises as fs } from 'fs';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
11
|
+
import type {
|
|
12
|
+
CacheEntry,
|
|
13
|
+
CacheMetadata,
|
|
14
|
+
CacheOptions,
|
|
15
|
+
CacheListOptions,
|
|
16
|
+
CacheStats,
|
|
17
|
+
CacheStatus,
|
|
18
|
+
CacheSource,
|
|
19
|
+
} from './types.js';
|
|
20
|
+
import { CacheError, CACHE_PATHS, DEFAULT_CACHE_CONFIG } from './types.js';
|
|
21
|
+
import type { CacheProvider } from './interfaces.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* No-op function for ignoring errors
|
|
25
|
+
*/
|
|
26
|
+
function noop(): void {
|
|
27
|
+
// Intentionally empty
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for creating a file cache provider
|
|
32
|
+
*/
|
|
33
|
+
export interface FileCacheProviderOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Base path for cache storage (project root)
|
|
36
|
+
*/
|
|
37
|
+
basePath: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Default TTL in seconds
|
|
41
|
+
*/
|
|
42
|
+
ttl?: number;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Maximum cache size in bytes
|
|
46
|
+
*/
|
|
47
|
+
maxSize?: number;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Maximum number of entries
|
|
51
|
+
*/
|
|
52
|
+
maxEntries?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Cache index structure for fast lookups
|
|
57
|
+
*/
|
|
58
|
+
interface CacheIndex {
|
|
59
|
+
version: number;
|
|
60
|
+
entries: Record<string, CacheIndexEntry>;
|
|
61
|
+
stats: {
|
|
62
|
+
totalSize: number;
|
|
63
|
+
entryCount: number;
|
|
64
|
+
lastCleanup: string | null;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Index entry for quick access
|
|
70
|
+
*/
|
|
71
|
+
interface CacheIndexEntry {
|
|
72
|
+
key: string;
|
|
73
|
+
source: CacheSource;
|
|
74
|
+
cachedAt: string;
|
|
75
|
+
expiresAt: string;
|
|
76
|
+
size: number;
|
|
77
|
+
contentFile: string;
|
|
78
|
+
metadataFile: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const INDEX_VERSION = 1;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* File-based cache provider implementation
|
|
85
|
+
*/
|
|
86
|
+
export class FileCacheProvider implements CacheProvider {
|
|
87
|
+
private basePath: string;
|
|
88
|
+
private cachePath: string;
|
|
89
|
+
private contentPath: string;
|
|
90
|
+
private metadataPath: string;
|
|
91
|
+
private indexPath: string;
|
|
92
|
+
private ttl: number;
|
|
93
|
+
private maxSize: number;
|
|
94
|
+
private maxEntries: number;
|
|
95
|
+
private initialized = false;
|
|
96
|
+
private index: CacheIndex | null = null;
|
|
97
|
+
|
|
98
|
+
constructor(options: FileCacheProviderOptions) {
|
|
99
|
+
this.basePath = options.basePath;
|
|
100
|
+
this.cachePath = join(this.basePath, CACHE_PATHS.ROOT);
|
|
101
|
+
this.contentPath = join(this.basePath, CACHE_PATHS.CONTENT);
|
|
102
|
+
this.metadataPath = join(this.basePath, CACHE_PATHS.METADATA);
|
|
103
|
+
this.indexPath = join(this.basePath, CACHE_PATHS.INDEX);
|
|
104
|
+
this.ttl = options.ttl ?? DEFAULT_CACHE_CONFIG.ttl;
|
|
105
|
+
this.maxSize = options.maxSize ?? DEFAULT_CACHE_CONFIG.maxSize;
|
|
106
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_CACHE_CONFIG.maxEntries;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Initialize the cache (create directories, load index)
|
|
111
|
+
*/
|
|
112
|
+
async initialize(): Promise<void> {
|
|
113
|
+
if (this.initialized) return;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Create cache directories
|
|
117
|
+
await fs.mkdir(this.contentPath, { recursive: true });
|
|
118
|
+
await fs.mkdir(this.metadataPath, { recursive: true });
|
|
119
|
+
|
|
120
|
+
// Load or create index
|
|
121
|
+
this.index = await this.loadIndex();
|
|
122
|
+
|
|
123
|
+
// Save index to ensure file exists
|
|
124
|
+
await this.saveIndex();
|
|
125
|
+
|
|
126
|
+
this.initialized = true;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw new CacheError(
|
|
129
|
+
`Failed to initialize cache: ${error instanceof Error ? error.message : String(error)}`,
|
|
130
|
+
'write_failed',
|
|
131
|
+
undefined,
|
|
132
|
+
error instanceof Error ? error : undefined
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if the cache is initialized
|
|
139
|
+
*/
|
|
140
|
+
isInitialized(): boolean {
|
|
141
|
+
return this.initialized;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get a cached entry by key
|
|
146
|
+
*/
|
|
147
|
+
async get<T = string>(key: string, options?: CacheOptions): Promise<CacheEntry<T> | null> {
|
|
148
|
+
this.ensureInitialized();
|
|
149
|
+
|
|
150
|
+
if (options?.skipCache) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const indexEntry = this.index?.entries[key];
|
|
155
|
+
if (!indexEntry) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check expiration (unless force refresh)
|
|
160
|
+
if (!options?.forceRefresh) {
|
|
161
|
+
const status = this.getEntryStatus(indexEntry);
|
|
162
|
+
if (status === 'expired') {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// Read metadata
|
|
169
|
+
const metadataContent = await fs.readFile(indexEntry.metadataFile, 'utf-8');
|
|
170
|
+
const metadata: CacheMetadata = JSON.parse(metadataContent);
|
|
171
|
+
|
|
172
|
+
// Read content
|
|
173
|
+
const contentRaw = await fs.readFile(indexEntry.contentFile, 'utf-8');
|
|
174
|
+
let content: T;
|
|
175
|
+
|
|
176
|
+
// Try to parse as JSON if content type indicates it
|
|
177
|
+
if (metadata.contentType.includes('json')) {
|
|
178
|
+
content = JSON.parse(contentRaw) as T;
|
|
179
|
+
} else {
|
|
180
|
+
content = contentRaw as T;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { metadata, content };
|
|
184
|
+
} catch {
|
|
185
|
+
// Entry exists in index but files are missing - clean up
|
|
186
|
+
await this.delete(key);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get the content only (convenience method)
|
|
193
|
+
*/
|
|
194
|
+
async getContent<T = string>(key: string, options?: CacheOptions): Promise<T | null> {
|
|
195
|
+
const entry = await this.get<T>(key, options);
|
|
196
|
+
return entry?.content ?? null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Set a cache entry
|
|
201
|
+
*/
|
|
202
|
+
async set<T = string>(
|
|
203
|
+
key: string,
|
|
204
|
+
content: T,
|
|
205
|
+
metadata: Partial<CacheMetadata>,
|
|
206
|
+
options?: CacheOptions
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
this.ensureInitialized();
|
|
209
|
+
|
|
210
|
+
const now = new Date();
|
|
211
|
+
const ttl = options?.ttl ?? this.ttl;
|
|
212
|
+
const expiresAt = new Date(now.getTime() + ttl * 1000);
|
|
213
|
+
|
|
214
|
+
// Serialize content
|
|
215
|
+
const contentStr = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
|
|
216
|
+
const contentHash = this.hashContent(contentStr);
|
|
217
|
+
const size = Buffer.byteLength(contentStr, 'utf-8');
|
|
218
|
+
|
|
219
|
+
// Generate file paths
|
|
220
|
+
const safeKey = this.sanitizeKey(key);
|
|
221
|
+
const contentFile = join(this.contentPath, `${safeKey}.cache`);
|
|
222
|
+
const metadataFile = join(this.metadataPath, `${safeKey}.json`);
|
|
223
|
+
|
|
224
|
+
// Build full metadata
|
|
225
|
+
const fullMetadata: CacheMetadata = {
|
|
226
|
+
key,
|
|
227
|
+
source: metadata.source ?? 'custom',
|
|
228
|
+
sourceUrl: metadata.sourceUrl ?? '',
|
|
229
|
+
cachedAt: now.toISOString(),
|
|
230
|
+
expiresAt: expiresAt.toISOString(),
|
|
231
|
+
contentHash,
|
|
232
|
+
size,
|
|
233
|
+
contentType:
|
|
234
|
+
metadata.contentType ?? (typeof content === 'string' ? 'text/plain' : 'application/json'),
|
|
235
|
+
};
|
|
236
|
+
// Conditionally add optional properties
|
|
237
|
+
if (metadata.etag) {
|
|
238
|
+
fullMetadata.etag = metadata.etag;
|
|
239
|
+
}
|
|
240
|
+
if (metadata.title) {
|
|
241
|
+
fullMetadata.title = metadata.title;
|
|
242
|
+
}
|
|
243
|
+
if (metadata.lastModified) {
|
|
244
|
+
fullMetadata.lastModified = metadata.lastModified;
|
|
245
|
+
}
|
|
246
|
+
const tags = options?.tags ?? metadata.tags;
|
|
247
|
+
if (tags) {
|
|
248
|
+
fullMetadata.tags = tags;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
// Ensure directories exist
|
|
253
|
+
await fs.mkdir(dirname(contentFile), { recursive: true });
|
|
254
|
+
await fs.mkdir(dirname(metadataFile), { recursive: true });
|
|
255
|
+
|
|
256
|
+
// Write files
|
|
257
|
+
await fs.writeFile(contentFile, contentStr, 'utf-8');
|
|
258
|
+
await fs.writeFile(metadataFile, JSON.stringify(fullMetadata, null, 2), 'utf-8');
|
|
259
|
+
|
|
260
|
+
// Update index
|
|
261
|
+
if (this.index) {
|
|
262
|
+
// Remove old size from stats if updating
|
|
263
|
+
const oldEntry = this.index.entries[key];
|
|
264
|
+
if (oldEntry) {
|
|
265
|
+
this.index.stats.totalSize -= oldEntry.size;
|
|
266
|
+
} else {
|
|
267
|
+
this.index.stats.entryCount++;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
this.index.entries[key] = {
|
|
271
|
+
key,
|
|
272
|
+
source: fullMetadata.source,
|
|
273
|
+
cachedAt: fullMetadata.cachedAt,
|
|
274
|
+
expiresAt: fullMetadata.expiresAt,
|
|
275
|
+
size,
|
|
276
|
+
contentFile,
|
|
277
|
+
metadataFile,
|
|
278
|
+
};
|
|
279
|
+
this.index.stats.totalSize += size;
|
|
280
|
+
|
|
281
|
+
await this.saveIndex();
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
throw new CacheError(
|
|
285
|
+
`Failed to write cache entry: ${error instanceof Error ? error.message : String(error)}`,
|
|
286
|
+
'write_failed',
|
|
287
|
+
key,
|
|
288
|
+
error instanceof Error ? error : undefined
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Check if a key exists in the cache
|
|
295
|
+
*/
|
|
296
|
+
async has(key: string): Promise<boolean> {
|
|
297
|
+
this.ensureInitialized();
|
|
298
|
+
return key in (this.index?.entries ?? {});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Delete a cache entry
|
|
303
|
+
*/
|
|
304
|
+
async delete(key: string): Promise<boolean> {
|
|
305
|
+
this.ensureInitialized();
|
|
306
|
+
|
|
307
|
+
const indexEntry = this.index?.entries[key];
|
|
308
|
+
if (!indexEntry) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
// Delete files (ignore if already missing)
|
|
314
|
+
await fs.unlink(indexEntry.contentFile).catch(noop);
|
|
315
|
+
await fs.unlink(indexEntry.metadataFile).catch(noop);
|
|
316
|
+
|
|
317
|
+
// Update index
|
|
318
|
+
if (this.index) {
|
|
319
|
+
this.index.stats.totalSize -= indexEntry.size;
|
|
320
|
+
this.index.stats.entryCount--;
|
|
321
|
+
// Remove entry from index using destructuring
|
|
322
|
+
const { [key]: _removed, ...remaining } = this.index.entries;
|
|
323
|
+
this.index.entries = remaining;
|
|
324
|
+
await this.saveIndex();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return true;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
throw new CacheError(
|
|
330
|
+
`Failed to delete cache entry: ${error instanceof Error ? error.message : String(error)}`,
|
|
331
|
+
'write_failed',
|
|
332
|
+
key,
|
|
333
|
+
error instanceof Error ? error : undefined
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get the status of a cache entry
|
|
340
|
+
*/
|
|
341
|
+
async getStatus(key: string): Promise<CacheStatus | null> {
|
|
342
|
+
this.ensureInitialized();
|
|
343
|
+
|
|
344
|
+
const indexEntry = this.index?.entries[key];
|
|
345
|
+
if (!indexEntry) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return this.getEntryStatus(indexEntry);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Get metadata for a cache entry
|
|
354
|
+
*/
|
|
355
|
+
async getMetadata(key: string): Promise<CacheMetadata | null> {
|
|
356
|
+
this.ensureInitialized();
|
|
357
|
+
|
|
358
|
+
const indexEntry = this.index?.entries[key];
|
|
359
|
+
if (!indexEntry) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const content = await fs.readFile(indexEntry.metadataFile, 'utf-8');
|
|
365
|
+
return JSON.parse(content);
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Update metadata for a cache entry
|
|
373
|
+
*/
|
|
374
|
+
async updateMetadata(key: string, metadata: Partial<CacheMetadata>): Promise<void> {
|
|
375
|
+
this.ensureInitialized();
|
|
376
|
+
|
|
377
|
+
const existing = await this.getMetadata(key);
|
|
378
|
+
if (!existing) {
|
|
379
|
+
throw new CacheError(`Cache entry not found: ${key}`, 'not_found', key);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const indexEntry = this.index?.entries[key];
|
|
383
|
+
if (!indexEntry) {
|
|
384
|
+
throw new CacheError(`Cache entry not found: ${key}`, 'not_found', key);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const updated: CacheMetadata = {
|
|
388
|
+
...existing,
|
|
389
|
+
...metadata,
|
|
390
|
+
key: existing.key, // Can't change key
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
await fs.writeFile(indexEntry.metadataFile, JSON.stringify(updated, null, 2), 'utf-8');
|
|
395
|
+
|
|
396
|
+
// Update index if relevant fields changed
|
|
397
|
+
if (this.index && indexEntry) {
|
|
398
|
+
if (metadata.source) indexEntry.source = metadata.source;
|
|
399
|
+
if (metadata.expiresAt) indexEntry.expiresAt = metadata.expiresAt;
|
|
400
|
+
await this.saveIndex();
|
|
401
|
+
}
|
|
402
|
+
} catch (error) {
|
|
403
|
+
throw new CacheError(
|
|
404
|
+
`Failed to update metadata: ${error instanceof Error ? error.message : String(error)}`,
|
|
405
|
+
'write_failed',
|
|
406
|
+
key,
|
|
407
|
+
error instanceof Error ? error : undefined
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* List cache entries
|
|
414
|
+
*/
|
|
415
|
+
async list(options?: CacheListOptions): Promise<CacheMetadata[]> {
|
|
416
|
+
this.ensureInitialized();
|
|
417
|
+
|
|
418
|
+
if (!this.index) return [];
|
|
419
|
+
|
|
420
|
+
let entries = Object.values(this.index.entries);
|
|
421
|
+
|
|
422
|
+
// Apply filters
|
|
423
|
+
if (options?.source) {
|
|
424
|
+
entries = entries.filter((e) => e.source === options.source);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (options?.status) {
|
|
428
|
+
entries = entries.filter((e) => this.getEntryStatus(e) === options.status);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Load full metadata for tag filtering and return
|
|
432
|
+
const metadataPromises = entries.map(async (e) => {
|
|
433
|
+
const metadata = await this.getMetadata(e.key);
|
|
434
|
+
return metadata;
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
let results = (await Promise.all(metadataPromises)).filter(
|
|
438
|
+
(m): m is CacheMetadata => m !== null
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// Filter by tag if specified
|
|
442
|
+
if (options?.tag) {
|
|
443
|
+
const tag = options.tag;
|
|
444
|
+
results = results.filter((m) => m.tags?.includes(tag));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Apply limit
|
|
448
|
+
if (options?.limit && results.length > options.limit) {
|
|
449
|
+
results = results.slice(0, options.limit);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return results;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Get cache statistics
|
|
457
|
+
*/
|
|
458
|
+
async getStats(): Promise<CacheStats> {
|
|
459
|
+
this.ensureInitialized();
|
|
460
|
+
|
|
461
|
+
if (!this.index) {
|
|
462
|
+
return {
|
|
463
|
+
totalEntries: 0,
|
|
464
|
+
totalSize: 0,
|
|
465
|
+
validEntries: 0,
|
|
466
|
+
staleEntries: 0,
|
|
467
|
+
expiredEntries: 0,
|
|
468
|
+
bySource: {
|
|
469
|
+
confluence: 0,
|
|
470
|
+
github: 0,
|
|
471
|
+
notion: 0,
|
|
472
|
+
local: 0,
|
|
473
|
+
custom: 0,
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const entries = Object.values(this.index.entries);
|
|
479
|
+
const bySource: Record<CacheSource, number> = {
|
|
480
|
+
confluence: 0,
|
|
481
|
+
github: 0,
|
|
482
|
+
notion: 0,
|
|
483
|
+
local: 0,
|
|
484
|
+
custom: 0,
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
let validEntries = 0;
|
|
488
|
+
let staleEntries = 0;
|
|
489
|
+
let expiredEntries = 0;
|
|
490
|
+
let oldestEntry: string | undefined;
|
|
491
|
+
let newestEntry: string | undefined;
|
|
492
|
+
|
|
493
|
+
for (const entry of entries) {
|
|
494
|
+
bySource[entry.source]++;
|
|
495
|
+
|
|
496
|
+
const status = this.getEntryStatus(entry);
|
|
497
|
+
if (status === 'valid') validEntries++;
|
|
498
|
+
else if (status === 'stale') staleEntries++;
|
|
499
|
+
else if (status === 'expired') expiredEntries++;
|
|
500
|
+
|
|
501
|
+
if (!oldestEntry || entry.cachedAt < oldestEntry) {
|
|
502
|
+
oldestEntry = entry.cachedAt;
|
|
503
|
+
}
|
|
504
|
+
if (!newestEntry || entry.cachedAt > newestEntry) {
|
|
505
|
+
newestEntry = entry.cachedAt;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const stats: CacheStats = {
|
|
510
|
+
totalEntries: this.index.stats.entryCount,
|
|
511
|
+
totalSize: this.index.stats.totalSize,
|
|
512
|
+
validEntries,
|
|
513
|
+
staleEntries,
|
|
514
|
+
expiredEntries,
|
|
515
|
+
bySource,
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
if (oldestEntry) stats.oldestEntry = oldestEntry;
|
|
519
|
+
if (newestEntry) stats.newestEntry = newestEntry;
|
|
520
|
+
|
|
521
|
+
return stats;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Clear all cache entries
|
|
526
|
+
*/
|
|
527
|
+
async clear(): Promise<number> {
|
|
528
|
+
this.ensureInitialized();
|
|
529
|
+
|
|
530
|
+
if (!this.index) return 0;
|
|
531
|
+
|
|
532
|
+
const count = this.index.stats.entryCount;
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
// Delete all content and metadata files
|
|
536
|
+
await fs.rm(this.contentPath, { recursive: true, force: true });
|
|
537
|
+
await fs.rm(this.metadataPath, { recursive: true, force: true });
|
|
538
|
+
|
|
539
|
+
// Recreate directories
|
|
540
|
+
await fs.mkdir(this.contentPath, { recursive: true });
|
|
541
|
+
await fs.mkdir(this.metadataPath, { recursive: true });
|
|
542
|
+
|
|
543
|
+
// Reset index
|
|
544
|
+
this.index = this.createEmptyIndex();
|
|
545
|
+
await this.saveIndex();
|
|
546
|
+
|
|
547
|
+
return count;
|
|
548
|
+
} catch (error) {
|
|
549
|
+
throw new CacheError(
|
|
550
|
+
`Failed to clear cache: ${error instanceof Error ? error.message : String(error)}`,
|
|
551
|
+
'write_failed',
|
|
552
|
+
undefined,
|
|
553
|
+
error instanceof Error ? error : undefined
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Clear expired entries only
|
|
560
|
+
*/
|
|
561
|
+
async clearExpired(): Promise<number> {
|
|
562
|
+
this.ensureInitialized();
|
|
563
|
+
|
|
564
|
+
if (!this.index) return 0;
|
|
565
|
+
|
|
566
|
+
const expiredKeys = Object.entries(this.index.entries)
|
|
567
|
+
.filter(([, entry]) => this.getEntryStatus(entry) === 'expired')
|
|
568
|
+
.map(([key]) => key);
|
|
569
|
+
|
|
570
|
+
for (const key of expiredKeys) {
|
|
571
|
+
await this.delete(key);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (this.index) {
|
|
575
|
+
this.index.stats.lastCleanup = new Date().toISOString();
|
|
576
|
+
await this.saveIndex();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return expiredKeys.length;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Clear entries by source
|
|
584
|
+
*/
|
|
585
|
+
async clearBySource(source: string): Promise<number> {
|
|
586
|
+
this.ensureInitialized();
|
|
587
|
+
|
|
588
|
+
if (!this.index) return 0;
|
|
589
|
+
|
|
590
|
+
const keys = Object.entries(this.index.entries)
|
|
591
|
+
.filter(([, entry]) => entry.source === source)
|
|
592
|
+
.map(([key]) => key);
|
|
593
|
+
|
|
594
|
+
for (const key of keys) {
|
|
595
|
+
await this.delete(key);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return keys.length;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Clear entries by tag
|
|
603
|
+
*/
|
|
604
|
+
async clearByTag(tag: string): Promise<number> {
|
|
605
|
+
this.ensureInitialized();
|
|
606
|
+
|
|
607
|
+
const entries = await this.list({ tag });
|
|
608
|
+
for (const entry of entries) {
|
|
609
|
+
await this.delete(entry.key);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return entries.length;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Private helpers
|
|
616
|
+
|
|
617
|
+
private ensureInitialized(): void {
|
|
618
|
+
if (!this.initialized) {
|
|
619
|
+
throw new CacheError('Cache not initialized. Call initialize() first.', 'invalid_config');
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private async loadIndex(): Promise<CacheIndex> {
|
|
624
|
+
try {
|
|
625
|
+
const content = await fs.readFile(this.indexPath, 'utf-8');
|
|
626
|
+
const index = JSON.parse(content) as CacheIndex;
|
|
627
|
+
|
|
628
|
+
// Validate version
|
|
629
|
+
if (index.version !== INDEX_VERSION) {
|
|
630
|
+
// Index version mismatch - rebuild
|
|
631
|
+
return this.rebuildIndex();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return index;
|
|
635
|
+
} catch {
|
|
636
|
+
// Index doesn't exist or is invalid - create new one
|
|
637
|
+
return this.createEmptyIndex();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private async saveIndex(): Promise<void> {
|
|
642
|
+
if (!this.index) return;
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
await fs.writeFile(this.indexPath, JSON.stringify(this.index, null, 2), 'utf-8');
|
|
646
|
+
} catch (error) {
|
|
647
|
+
throw new CacheError(
|
|
648
|
+
`Failed to save cache index: ${error instanceof Error ? error.message : String(error)}`,
|
|
649
|
+
'write_failed',
|
|
650
|
+
undefined,
|
|
651
|
+
error instanceof Error ? error : undefined
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private createEmptyIndex(): CacheIndex {
|
|
657
|
+
return {
|
|
658
|
+
version: INDEX_VERSION,
|
|
659
|
+
entries: {},
|
|
660
|
+
stats: {
|
|
661
|
+
totalSize: 0,
|
|
662
|
+
entryCount: 0,
|
|
663
|
+
lastCleanup: null,
|
|
664
|
+
},
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
private async rebuildIndex(): Promise<CacheIndex> {
|
|
669
|
+
const index = this.createEmptyIndex();
|
|
670
|
+
|
|
671
|
+
try {
|
|
672
|
+
const metadataFiles = await fs.readdir(this.metadataPath);
|
|
673
|
+
|
|
674
|
+
for (const file of metadataFiles) {
|
|
675
|
+
if (!file.endsWith('.json')) continue;
|
|
676
|
+
|
|
677
|
+
try {
|
|
678
|
+
const metadataPath = join(this.metadataPath, file);
|
|
679
|
+
const content = await fs.readFile(metadataPath, 'utf-8');
|
|
680
|
+
const metadata: CacheMetadata = JSON.parse(content);
|
|
681
|
+
|
|
682
|
+
const safeKey = this.sanitizeKey(metadata.key);
|
|
683
|
+
const contentFile = join(this.contentPath, `${safeKey}.cache`);
|
|
684
|
+
|
|
685
|
+
// Verify content file exists
|
|
686
|
+
await fs.access(contentFile);
|
|
687
|
+
|
|
688
|
+
index.entries[metadata.key] = {
|
|
689
|
+
key: metadata.key,
|
|
690
|
+
source: metadata.source,
|
|
691
|
+
cachedAt: metadata.cachedAt,
|
|
692
|
+
expiresAt: metadata.expiresAt,
|
|
693
|
+
size: metadata.size,
|
|
694
|
+
contentFile,
|
|
695
|
+
metadataFile: metadataPath,
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
index.stats.totalSize += metadata.size;
|
|
699
|
+
index.stats.entryCount++;
|
|
700
|
+
} catch {
|
|
701
|
+
// Skip invalid entries
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
} catch {
|
|
705
|
+
// Metadata directory doesn't exist or is empty
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return index;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
private getEntryStatus(entry: CacheIndexEntry): CacheStatus {
|
|
712
|
+
const now = new Date();
|
|
713
|
+
const expiresAt = new Date(entry.expiresAt);
|
|
714
|
+
const cachedAt = new Date(entry.cachedAt);
|
|
715
|
+
|
|
716
|
+
if (now > expiresAt) {
|
|
717
|
+
return 'expired';
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Consider stale if more than 80% of TTL has passed
|
|
721
|
+
const totalTtl = expiresAt.getTime() - cachedAt.getTime();
|
|
722
|
+
const elapsed = now.getTime() - cachedAt.getTime();
|
|
723
|
+
if (elapsed > totalTtl * 0.8) {
|
|
724
|
+
return 'stale';
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return 'valid';
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private sanitizeKey(key: string): string {
|
|
731
|
+
// Create a safe filename from the key
|
|
732
|
+
return createHash('sha256').update(key).digest('hex').substring(0, 32);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
private hashContent(content: string): string {
|
|
736
|
+
return createHash('sha256').update(content).digest('hex');
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Create a file cache provider
|
|
742
|
+
*/
|
|
743
|
+
export function createFileCacheProvider(options: FileCacheProviderOptions): FileCacheProvider {
|
|
744
|
+
return new FileCacheProvider(options);
|
|
745
|
+
}
|