@unrdf/kgc-runtime 26.4.2
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/IMPLEMENTATION_SUMMARY.json +150 -0
- package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
- package/README.md +98 -0
- package/TRANSACTION_IMPLEMENTATION.json +119 -0
- package/capability-map.md +93 -0
- package/docs/api-stability.md +269 -0
- package/docs/extensions/plugin-development.md +382 -0
- package/package.json +40 -0
- package/plugins/registry.json +35 -0
- package/src/admission-gate.mjs +414 -0
- package/src/api-version.mjs +373 -0
- package/src/atomic-admission.mjs +310 -0
- package/src/bounds.mjs +289 -0
- package/src/bulkhead-manager.mjs +280 -0
- package/src/capsule.mjs +524 -0
- package/src/crdt.mjs +361 -0
- package/src/enhanced-bounds.mjs +614 -0
- package/src/executor.mjs +73 -0
- package/src/freeze-restore.mjs +521 -0
- package/src/index.mjs +62 -0
- package/src/materialized-views.mjs +371 -0
- package/src/merge.mjs +472 -0
- package/src/plugin-isolation.mjs +392 -0
- package/src/plugin-manager.mjs +441 -0
- package/src/projections-api.mjs +336 -0
- package/src/projections-cli.mjs +238 -0
- package/src/projections-docs.mjs +300 -0
- package/src/projections-ide.mjs +278 -0
- package/src/receipt.mjs +340 -0
- package/src/rollback.mjs +258 -0
- package/src/saga-orchestrator.mjs +355 -0
- package/src/schemas.mjs +1330 -0
- package/src/storage-optimization.mjs +359 -0
- package/src/tool-registry.mjs +272 -0
- package/src/transaction.mjs +466 -0
- package/src/validators.mjs +485 -0
- package/src/work-item.mjs +449 -0
- package/templates/plugin-template/README.md +58 -0
- package/templates/plugin-template/index.mjs +162 -0
- package/templates/plugin-template/plugin.json +19 -0
- package/test/admission-gate.test.mjs +583 -0
- package/test/api-version.test.mjs +74 -0
- package/test/atomic-admission.test.mjs +155 -0
- package/test/bounds.test.mjs +341 -0
- package/test/bulkhead-manager.test.mjs +236 -0
- package/test/capsule.test.mjs +625 -0
- package/test/crdt.test.mjs +215 -0
- package/test/enhanced-bounds.test.mjs +487 -0
- package/test/freeze-restore.test.mjs +472 -0
- package/test/materialized-views.test.mjs +243 -0
- package/test/merge.test.mjs +665 -0
- package/test/plugin-isolation.test.mjs +109 -0
- package/test/plugin-manager.test.mjs +208 -0
- package/test/projections-api.test.mjs +293 -0
- package/test/projections-cli.test.mjs +204 -0
- package/test/projections-docs.test.mjs +173 -0
- package/test/projections-ide.test.mjs +230 -0
- package/test/receipt.test.mjs +295 -0
- package/test/rollback.test.mjs +132 -0
- package/test/saga-orchestrator.test.mjs +279 -0
- package/test/schemas.test.mjs +716 -0
- package/test/storage-optimization.test.mjs +503 -0
- package/test/tool-registry.test.mjs +341 -0
- package/test/transaction.test.mjs +189 -0
- package/test/validators.test.mjs +463 -0
- package/test/work-item.test.mjs +548 -0
- package/test/work-item.test.mjs.bak +548 -0
- package/var/kgc/test-atomic-log.json +519 -0
- package/var/kgc/test-cascading-log.json +145 -0
- package/vitest.config.mjs +18 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Storage optimization utilities for KGC runtime
|
|
3
|
+
* Provides compression, garbage collection, and archival capabilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { promises as fs } from 'fs';
|
|
7
|
+
import { createGzip, createGunzip } from 'zlib';
|
|
8
|
+
import { pipeline } from 'stream/promises';
|
|
9
|
+
import { createReadStream, createWriteStream } from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Compress file using gzip (level 6)
|
|
14
|
+
* @param {string} inputPath - Path to input file
|
|
15
|
+
* @param {string} outputPath - Path to output file (will be .gz)
|
|
16
|
+
* @returns {Promise<{original_size: number, compressed_size: number}>} Size metadata
|
|
17
|
+
*/
|
|
18
|
+
export async function compressFile(inputPath, outputPath) {
|
|
19
|
+
const stats = await fs.stat(inputPath);
|
|
20
|
+
const originalSize = stats.size;
|
|
21
|
+
|
|
22
|
+
const gzip = createGzip({ level: 6 });
|
|
23
|
+
const source = createReadStream(inputPath);
|
|
24
|
+
const destination = createWriteStream(outputPath);
|
|
25
|
+
|
|
26
|
+
await pipeline(source, gzip, destination);
|
|
27
|
+
|
|
28
|
+
const compressedStats = await fs.stat(outputPath);
|
|
29
|
+
const compressedSize = compressedStats.size;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
original_size: originalSize,
|
|
33
|
+
compressed_size: compressedSize,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Decompress gzip file
|
|
39
|
+
* @param {string} inputPath - Path to .gz file
|
|
40
|
+
* @param {string} outputPath - Path to output file
|
|
41
|
+
* @returns {Promise<void>}
|
|
42
|
+
*/
|
|
43
|
+
export async function decompressFile(inputPath, outputPath) {
|
|
44
|
+
const gunzip = createGunzip();
|
|
45
|
+
const source = createReadStream(inputPath);
|
|
46
|
+
const destination = createWriteStream(outputPath);
|
|
47
|
+
|
|
48
|
+
await pipeline(source, gunzip, destination);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read and decompress gzip file to string
|
|
53
|
+
* @param {string} filePath - Path to .gz file
|
|
54
|
+
* @returns {Promise<string>} Decompressed content
|
|
55
|
+
*/
|
|
56
|
+
export async function readCompressed(filePath) {
|
|
57
|
+
const chunks = [];
|
|
58
|
+
const gunzip = createGunzip();
|
|
59
|
+
const source = createReadStream(filePath);
|
|
60
|
+
|
|
61
|
+
await pipeline(
|
|
62
|
+
source,
|
|
63
|
+
gunzip,
|
|
64
|
+
async function* (source) {
|
|
65
|
+
for await (const chunk of source) {
|
|
66
|
+
chunks.push(chunk);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Garbage collection configuration
|
|
76
|
+
* @typedef {Object} GCConfig
|
|
77
|
+
* @property {number} maxSnapshots - Maximum number of snapshots to keep
|
|
78
|
+
* @property {number} ttlDays - Time-to-live in days
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Apply garbage collection to snapshots
|
|
83
|
+
* @param {string} snapshotDir - Snapshot directory path
|
|
84
|
+
* @param {GCConfig} config - GC configuration
|
|
85
|
+
* @returns {Promise<{deleted: number, kept: number, bytes_freed: number}>} GC results
|
|
86
|
+
*/
|
|
87
|
+
export async function garbageCollectSnapshots(
|
|
88
|
+
snapshotDir,
|
|
89
|
+
config = { maxSnapshots: 100, ttlDays: 30 }
|
|
90
|
+
) {
|
|
91
|
+
try {
|
|
92
|
+
await fs.access(snapshotDir);
|
|
93
|
+
} catch {
|
|
94
|
+
return { deleted: 0, kept: 0, bytes_freed: 0 };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const entries = await fs.readdir(snapshotDir, { withFileTypes: true });
|
|
98
|
+
const snapshotDirs = entries
|
|
99
|
+
.filter((entry) => entry.isDirectory())
|
|
100
|
+
.map((entry) => entry.name);
|
|
101
|
+
|
|
102
|
+
const snapshots = [];
|
|
103
|
+
for (const dirName of snapshotDirs) {
|
|
104
|
+
try {
|
|
105
|
+
const manifestPath = path.join(snapshotDir, dirName, 'manifest.json');
|
|
106
|
+
const manifestData = await fs.readFile(manifestPath, 'utf-8');
|
|
107
|
+
const manifest = JSON.parse(manifestData);
|
|
108
|
+
|
|
109
|
+
snapshots.push({
|
|
110
|
+
dirName,
|
|
111
|
+
path: path.join(snapshotDir, dirName),
|
|
112
|
+
manifest,
|
|
113
|
+
timestamp_ns: BigInt(manifest.timestamp_ns),
|
|
114
|
+
created_at: new Date(manifest.created_at),
|
|
115
|
+
});
|
|
116
|
+
} catch {
|
|
117
|
+
// Skip invalid snapshots
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Sort by timestamp (newest first)
|
|
122
|
+
snapshots.sort((a, b) => (a.timestamp_ns > b.timestamp_ns ? -1 : 1));
|
|
123
|
+
|
|
124
|
+
const now = new Date();
|
|
125
|
+
const ttlMs = config.ttlDays * 24 * 60 * 60 * 1000;
|
|
126
|
+
const cutoffDate = new Date(now.getTime() - ttlMs);
|
|
127
|
+
|
|
128
|
+
let deleted = 0;
|
|
129
|
+
let bytesFreed = 0;
|
|
130
|
+
|
|
131
|
+
for (let i = 0; i < snapshots.length; i++) {
|
|
132
|
+
const snapshot = snapshots[i];
|
|
133
|
+
const shouldDelete =
|
|
134
|
+
i >= config.maxSnapshots || snapshot.created_at < cutoffDate;
|
|
135
|
+
|
|
136
|
+
if (shouldDelete) {
|
|
137
|
+
try {
|
|
138
|
+
// Calculate size before deletion
|
|
139
|
+
const size = await getDirectorySize(snapshot.path);
|
|
140
|
+
await fs.rm(snapshot.path, { recursive: true, force: true });
|
|
141
|
+
deleted++;
|
|
142
|
+
bytesFreed += size;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
// Continue if deletion fails
|
|
145
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
146
|
+
console.warn(
|
|
147
|
+
`[GC] Failed to delete snapshot ${snapshot.dirName}: ${error.message}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
deleted,
|
|
156
|
+
kept: snapshots.length - deleted,
|
|
157
|
+
bytes_freed: bytesFreed,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get total size of directory
|
|
163
|
+
* @param {string} dirPath - Directory path
|
|
164
|
+
* @returns {Promise<number>} Total bytes
|
|
165
|
+
* @private
|
|
166
|
+
*/
|
|
167
|
+
async function getDirectorySize(dirPath) {
|
|
168
|
+
let totalSize = 0;
|
|
169
|
+
|
|
170
|
+
async function traverse(currentPath) {
|
|
171
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
172
|
+
|
|
173
|
+
for (const entry of entries) {
|
|
174
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
175
|
+
|
|
176
|
+
if (entry.isDirectory()) {
|
|
177
|
+
await traverse(fullPath);
|
|
178
|
+
} else {
|
|
179
|
+
const stats = await fs.stat(fullPath);
|
|
180
|
+
totalSize += stats.size;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await traverse(dirPath);
|
|
186
|
+
return totalSize;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Receipt archival configuration
|
|
191
|
+
* @typedef {Object} ArchivalConfig
|
|
192
|
+
* @property {number} keepRecent - Number of recent receipts to keep
|
|
193
|
+
* @property {number} keepDays - Days to keep receipts in main store
|
|
194
|
+
*/
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Archive old receipts
|
|
198
|
+
* @param {string} receiptDir - Receipt directory path
|
|
199
|
+
* @param {string} archiveDir - Archive directory path
|
|
200
|
+
* @param {ArchivalConfig} config - Archival configuration
|
|
201
|
+
* @returns {Promise<{archived: number, kept: number}>} Archival results
|
|
202
|
+
*/
|
|
203
|
+
export async function archiveReceipts(
|
|
204
|
+
receiptDir,
|
|
205
|
+
archiveDir,
|
|
206
|
+
config = { keepRecent: 1000, keepDays: 7 }
|
|
207
|
+
) {
|
|
208
|
+
try {
|
|
209
|
+
await fs.access(receiptDir);
|
|
210
|
+
} catch {
|
|
211
|
+
return { archived: 0, kept: 0 };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
await fs.mkdir(archiveDir, { recursive: true });
|
|
215
|
+
|
|
216
|
+
const files = await fs.readdir(receiptDir);
|
|
217
|
+
const receiptFiles = files.filter((f) => f.startsWith('receipt-') && f.endsWith('.json'));
|
|
218
|
+
|
|
219
|
+
const receipts = [];
|
|
220
|
+
for (const file of receiptFiles) {
|
|
221
|
+
try {
|
|
222
|
+
const filePath = path.join(receiptDir, file);
|
|
223
|
+
const stats = await fs.stat(filePath);
|
|
224
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
225
|
+
const receipt = JSON.parse(data);
|
|
226
|
+
|
|
227
|
+
receipts.push({
|
|
228
|
+
file,
|
|
229
|
+
path: filePath,
|
|
230
|
+
receipt,
|
|
231
|
+
timestamp: new Date(receipt.timestamp),
|
|
232
|
+
modified: stats.mtime,
|
|
233
|
+
});
|
|
234
|
+
} catch {
|
|
235
|
+
// Skip invalid files
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Sort by timestamp (newest first)
|
|
240
|
+
receipts.sort((a, b) => b.timestamp - a.timestamp);
|
|
241
|
+
|
|
242
|
+
const now = new Date();
|
|
243
|
+
const keepMs = config.keepDays * 24 * 60 * 60 * 1000;
|
|
244
|
+
const cutoffDate = new Date(now.getTime() - keepMs);
|
|
245
|
+
|
|
246
|
+
let archived = 0;
|
|
247
|
+
|
|
248
|
+
for (let i = 0; i < receipts.length; i++) {
|
|
249
|
+
const receipt = receipts[i];
|
|
250
|
+
const shouldArchive = i >= config.keepRecent || receipt.timestamp < cutoffDate;
|
|
251
|
+
|
|
252
|
+
if (shouldArchive) {
|
|
253
|
+
try {
|
|
254
|
+
const archivePath = path.join(archiveDir, receipt.file);
|
|
255
|
+
await fs.rename(receipt.path, archivePath);
|
|
256
|
+
archived++;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
// Continue if archival fails
|
|
259
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
260
|
+
console.warn(
|
|
261
|
+
`[Archive] Failed to archive receipt ${receipt.file}: ${error.message}`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
archived,
|
|
270
|
+
kept: receipts.length - archived,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Compute delta between two objects (for incremental snapshots)
|
|
276
|
+
* @param {Object} previous - Previous state
|
|
277
|
+
* @param {Object} current - Current state
|
|
278
|
+
* @returns {Object} Delta object with changes
|
|
279
|
+
*/
|
|
280
|
+
export function computeDelta(previous, current) {
|
|
281
|
+
const delta = {
|
|
282
|
+
added: {},
|
|
283
|
+
modified: {},
|
|
284
|
+
deleted: {},
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const prevStr = JSON.stringify(previous);
|
|
288
|
+
const currStr = JSON.stringify(current);
|
|
289
|
+
|
|
290
|
+
// If identical, return empty delta
|
|
291
|
+
if (prevStr === currStr) {
|
|
292
|
+
return delta;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Simple delta: store full current state if different
|
|
296
|
+
// Phase 1: basic implementation (can be optimized later)
|
|
297
|
+
const prevKeys = new Set(Object.keys(previous || {}));
|
|
298
|
+
const currKeys = new Set(Object.keys(current || {}));
|
|
299
|
+
|
|
300
|
+
// Added keys
|
|
301
|
+
for (const key of currKeys) {
|
|
302
|
+
if (!prevKeys.has(key)) {
|
|
303
|
+
delta.added[key] = current[key];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Modified keys
|
|
308
|
+
for (const key of currKeys) {
|
|
309
|
+
if (prevKeys.has(key)) {
|
|
310
|
+
const prevValue = JSON.stringify(previous[key]);
|
|
311
|
+
const currValue = JSON.stringify(current[key]);
|
|
312
|
+
if (prevValue !== currValue) {
|
|
313
|
+
delta.modified[key] = current[key];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Deleted keys
|
|
319
|
+
for (const key of prevKeys) {
|
|
320
|
+
if (!currKeys.has(key)) {
|
|
321
|
+
delta.deleted[key] = true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return delta;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Apply delta to base state
|
|
330
|
+
* @param {Object} base - Base state
|
|
331
|
+
* @param {Object} delta - Delta to apply
|
|
332
|
+
* @returns {Object} Reconstructed state
|
|
333
|
+
*/
|
|
334
|
+
export function applyDelta(base, delta) {
|
|
335
|
+
const result = { ...base };
|
|
336
|
+
|
|
337
|
+
// Apply additions
|
|
338
|
+
if (delta.added) {
|
|
339
|
+
for (const [key, value] of Object.entries(delta.added)) {
|
|
340
|
+
result[key] = value;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Apply modifications
|
|
345
|
+
if (delta.modified) {
|
|
346
|
+
for (const [key, value] of Object.entries(delta.modified)) {
|
|
347
|
+
result[key] = value;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Apply deletions
|
|
352
|
+
if (delta.deleted) {
|
|
353
|
+
for (const key of Object.keys(delta.deleted)) {
|
|
354
|
+
delete result[key];
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tool Registry - Central repository for tool manifests
|
|
3
|
+
* @module @unrdf/kgc-runtime/tool-registry
|
|
4
|
+
* @description Manages tool manifests, versioning, and capability queries
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname, join } from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tool manifest schema
|
|
14
|
+
*/
|
|
15
|
+
const ManifestSchema = z.object({
|
|
16
|
+
name: z.string(),
|
|
17
|
+
version: z.string(),
|
|
18
|
+
description: z.string().optional(),
|
|
19
|
+
schema_in: z.any(), // Will be converted to Zod schema
|
|
20
|
+
schema_out: z.any(), // Will be converted to Zod schema
|
|
21
|
+
capabilities: z.array(z.string()),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Tool Registry class for managing tool manifests
|
|
26
|
+
*/
|
|
27
|
+
export class ToolRegistry {
|
|
28
|
+
/**
|
|
29
|
+
* Create a new ToolRegistry
|
|
30
|
+
* @param {Object} options - Configuration options
|
|
31
|
+
* @param {string} options.registryPath - Path to registry JSON file
|
|
32
|
+
*/
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
/** @type {Map<string, Object>} */
|
|
35
|
+
this.tools = new Map();
|
|
36
|
+
|
|
37
|
+
/** @type {Map<string, Map<string, Object>>} */
|
|
38
|
+
this.versionedTools = new Map();
|
|
39
|
+
|
|
40
|
+
this.registryPath = options.registryPath;
|
|
41
|
+
|
|
42
|
+
if (this.registryPath) {
|
|
43
|
+
this.loadFromFile(this.registryPath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load tool manifests from JSON file
|
|
49
|
+
* @param {string} filePath - Path to registry JSON
|
|
50
|
+
*/
|
|
51
|
+
loadFromFile(filePath) {
|
|
52
|
+
try {
|
|
53
|
+
const data = readFileSync(filePath, 'utf-8');
|
|
54
|
+
const registry = JSON.parse(data);
|
|
55
|
+
|
|
56
|
+
if (registry.tools && Array.isArray(registry.tools)) {
|
|
57
|
+
for (const toolData of registry.tools) {
|
|
58
|
+
this.registerTool(toolData);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw new Error(`Failed to load registry from ${filePath}: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Register a tool manifest
|
|
68
|
+
* @param {Object} manifest - Tool manifest
|
|
69
|
+
* @throws {Error} If manifest is invalid
|
|
70
|
+
*/
|
|
71
|
+
registerTool(manifest) {
|
|
72
|
+
// Validate manifest structure
|
|
73
|
+
ManifestSchema.parse(manifest);
|
|
74
|
+
|
|
75
|
+
const { name, version } = manifest;
|
|
76
|
+
|
|
77
|
+
// Convert schema definitions to Zod schemas if they're objects
|
|
78
|
+
const processedManifest = {
|
|
79
|
+
...manifest,
|
|
80
|
+
schema_in:
|
|
81
|
+
typeof manifest.schema_in === 'object' &&
|
|
82
|
+
!manifest.schema_in._def
|
|
83
|
+
? this.convertToZodSchema(manifest.schema_in)
|
|
84
|
+
: manifest.schema_in,
|
|
85
|
+
schema_out:
|
|
86
|
+
typeof manifest.schema_out === 'object' &&
|
|
87
|
+
!manifest.schema_out._def
|
|
88
|
+
? this.convertToZodSchema(manifest.schema_out)
|
|
89
|
+
: manifest.schema_out,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Store latest version by name
|
|
93
|
+
this.tools.set(name, processedManifest);
|
|
94
|
+
|
|
95
|
+
// Store versioned tool
|
|
96
|
+
if (!this.versionedTools.has(name)) {
|
|
97
|
+
this.versionedTools.set(name, new Map());
|
|
98
|
+
}
|
|
99
|
+
this.versionedTools.get(name).set(version, processedManifest);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Convert schema definition to Zod schema
|
|
104
|
+
* @param {Object} schemaDef - Schema definition
|
|
105
|
+
* @returns {z.ZodSchema} Zod schema
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
convertToZodSchema(schemaDef) {
|
|
109
|
+
if (!schemaDef.type) {
|
|
110
|
+
return z.any();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
switch (schemaDef.type) {
|
|
114
|
+
case 'object': {
|
|
115
|
+
const shape = {};
|
|
116
|
+
if (schemaDef.properties) {
|
|
117
|
+
for (const [key, propDef] of Object.entries(
|
|
118
|
+
schemaDef.properties,
|
|
119
|
+
)) {
|
|
120
|
+
let fieldSchema = this.convertToZodSchema(propDef);
|
|
121
|
+
|
|
122
|
+
// Handle optional fields
|
|
123
|
+
if (
|
|
124
|
+
schemaDef.required &&
|
|
125
|
+
!schemaDef.required.includes(key)
|
|
126
|
+
) {
|
|
127
|
+
fieldSchema = fieldSchema.optional();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
shape[key] = fieldSchema;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return z.object(shape);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case 'string':
|
|
137
|
+
return z.string();
|
|
138
|
+
|
|
139
|
+
case 'number':
|
|
140
|
+
return z.number();
|
|
141
|
+
|
|
142
|
+
case 'boolean':
|
|
143
|
+
return z.boolean();
|
|
144
|
+
|
|
145
|
+
case 'array':
|
|
146
|
+
return z.array(
|
|
147
|
+
schemaDef.items
|
|
148
|
+
? this.convertToZodSchema(schemaDef.items)
|
|
149
|
+
: z.any(),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
case 'null':
|
|
153
|
+
return z.null();
|
|
154
|
+
|
|
155
|
+
default:
|
|
156
|
+
return z.any();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get tool manifest by name (latest version)
|
|
162
|
+
* @param {string} name - Tool name
|
|
163
|
+
* @returns {Object|null} Tool manifest or null if not found
|
|
164
|
+
*/
|
|
165
|
+
getTool(name) {
|
|
166
|
+
return this.tools.get(name) || null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get tool manifest by name and version
|
|
171
|
+
* @param {string} name - Tool name
|
|
172
|
+
* @param {string} version - Tool version
|
|
173
|
+
* @returns {Object|null} Tool manifest or null if not found
|
|
174
|
+
*/
|
|
175
|
+
getToolVersion(name, version) {
|
|
176
|
+
const versions = this.versionedTools.get(name);
|
|
177
|
+
if (!versions) return null;
|
|
178
|
+
return versions.get(version) || null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get all tool manifests
|
|
183
|
+
* @returns {Array<Object>} Array of all tool manifests
|
|
184
|
+
*/
|
|
185
|
+
getAllTools() {
|
|
186
|
+
return Array.from(this.tools.values());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get all versions of a tool
|
|
191
|
+
* @param {string} name - Tool name
|
|
192
|
+
* @returns {Array<Object>} Array of tool manifests for all versions
|
|
193
|
+
*/
|
|
194
|
+
getAllVersions(name) {
|
|
195
|
+
const versions = this.versionedTools.get(name);
|
|
196
|
+
if (!versions) return [];
|
|
197
|
+
return Array.from(versions.values());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Query tools by capability
|
|
202
|
+
* @param {string} capability - Capability to search for
|
|
203
|
+
* @returns {Array<Object>} Array of tools with the capability
|
|
204
|
+
*/
|
|
205
|
+
getToolsByCapability(capability) {
|
|
206
|
+
return this.getAllTools().filter((tool) =>
|
|
207
|
+
tool.capabilities.includes(capability),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Check if tool supports capability
|
|
213
|
+
* @param {string} toolName - Tool name
|
|
214
|
+
* @param {string} capability - Capability to check
|
|
215
|
+
* @returns {boolean} True if tool supports capability
|
|
216
|
+
*/
|
|
217
|
+
hasCapability(toolName, capability) {
|
|
218
|
+
const tool = this.getTool(toolName);
|
|
219
|
+
if (!tool) return false;
|
|
220
|
+
return tool.capabilities.includes(capability);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Validate tool output against registered schema
|
|
225
|
+
* @param {string} toolName - Tool name
|
|
226
|
+
* @param {*} output - Output to validate
|
|
227
|
+
* @returns {boolean} True if valid
|
|
228
|
+
*/
|
|
229
|
+
validateOutput(toolName, output) {
|
|
230
|
+
const tool = this.getTool(toolName);
|
|
231
|
+
if (!tool) {
|
|
232
|
+
throw new Error(`Tool ${toolName} not found in registry`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
tool.schema_out.parse(output);
|
|
237
|
+
return true;
|
|
238
|
+
} catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get registry statistics
|
|
245
|
+
* @returns {Object} Statistics about the registry
|
|
246
|
+
*/
|
|
247
|
+
getStats() {
|
|
248
|
+
const tools = this.getAllTools();
|
|
249
|
+
const capabilities = new Set();
|
|
250
|
+
|
|
251
|
+
for (const tool of tools) {
|
|
252
|
+
for (const cap of tool.capabilities) {
|
|
253
|
+
capabilities.add(cap);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
total_tools: tools.length,
|
|
259
|
+
unique_capabilities: capabilities.size,
|
|
260
|
+
capabilities: Array.from(capabilities),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Create a ToolRegistry instance with default registry
|
|
267
|
+
* @param {string} registryPath - Optional path to registry file
|
|
268
|
+
* @returns {ToolRegistry} New ToolRegistry instance
|
|
269
|
+
*/
|
|
270
|
+
export function createRegistry(registryPath) {
|
|
271
|
+
return new ToolRegistry({ registryPath });
|
|
272
|
+
}
|