@team-semicolon/semo-cli 3.14.1 → 4.0.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/dist/kb.js ADDED
@@ -0,0 +1,627 @@
1
+ "use strict";
2
+ /**
3
+ * SEMO KB/Ontology Module
4
+ *
5
+ * Knowledge Base and Ontology management for SEMO bot ecosystem.
6
+ * Uses the team's core PostgreSQL database as Single Source of Truth.
7
+ *
8
+ * v3.15.0: Initial implementation
9
+ * v3.15.1: pgvector embedding integration
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.generateEmbedding = generateEmbedding;
46
+ exports.generateEmbeddings = generateEmbeddings;
47
+ exports.kbPull = kbPull;
48
+ exports.kbPush = kbPush;
49
+ exports.kbStatus = kbStatus;
50
+ exports.kbList = kbList;
51
+ exports.kbDiff = kbDiff;
52
+ exports.kbSearch = kbSearch;
53
+ exports.ontoList = ontoList;
54
+ exports.ontoShow = ontoShow;
55
+ exports.ontoValidate = ontoValidate;
56
+ exports.ontoPullToLocal = ontoPullToLocal;
57
+ const fs = __importStar(require("fs"));
58
+ const path = __importStar(require("path"));
59
+ // ============================================================
60
+ // Embedding
61
+ // ============================================================
62
+ const EMBEDDING_MODEL = "voyage-3";
63
+ const EMBEDDING_DIMENSIONS = 1024;
64
+ /**
65
+ * Generate embedding vector for text using OpenAI API
66
+ * Requires OPENAI_API_KEY environment variable
67
+ */
68
+ async function generateEmbedding(text) {
69
+ const apiKey = process.env.VOYAGE_API_KEY;
70
+ if (!apiKey)
71
+ return null;
72
+ try {
73
+ const response = await fetch("https://api.voyageai.com/v1/embeddings", {
74
+ method: "POST",
75
+ headers: {
76
+ "Authorization": `Bearer ${apiKey}`,
77
+ "Content-Type": "application/json",
78
+ },
79
+ body: JSON.stringify({
80
+ model: EMBEDDING_MODEL,
81
+ input: text.substring(0, 8000), // truncate to avoid token limit
82
+ dimensions: EMBEDDING_DIMENSIONS,
83
+ }),
84
+ });
85
+ if (!response.ok) {
86
+ const err = await response.text();
87
+ console.error(`Embedding API error: ${response.status} ${err}`);
88
+ return null;
89
+ }
90
+ const data = await response.json();
91
+ return data.data?.[0]?.embedding || null;
92
+ }
93
+ catch (err) {
94
+ console.error(`Embedding error: ${err}`);
95
+ return null;
96
+ }
97
+ }
98
+ /**
99
+ * Generate embeddings for multiple texts (batched)
100
+ */
101
+ async function generateEmbeddings(texts) {
102
+ const apiKey = process.env.VOYAGE_API_KEY;
103
+ if (!apiKey)
104
+ return texts.map(() => null);
105
+ try {
106
+ const response = await fetch("https://api.voyageai.com/v1/embeddings", {
107
+ method: "POST",
108
+ headers: {
109
+ "Authorization": `Bearer ${apiKey}`,
110
+ "Content-Type": "application/json",
111
+ },
112
+ body: JSON.stringify({
113
+ model: EMBEDDING_MODEL,
114
+ input: texts.map(t => t.substring(0, 16000)),
115
+ output_dimension: EMBEDDING_DIMENSIONS,
116
+ }),
117
+ });
118
+ if (!response.ok)
119
+ return texts.map(() => null);
120
+ const data = await response.json();
121
+ return data.data?.map((d) => d.embedding) || texts.map(() => null);
122
+ }
123
+ catch {
124
+ return texts.map(() => null);
125
+ }
126
+ }
127
+ // ============================================================
128
+ // KB Directory Management
129
+ // ============================================================
130
+ const KB_DIR = ".kb";
131
+ const SYNC_STATE_FILE = ".sync-state.json";
132
+ function getKBDir(cwd) {
133
+ return path.join(cwd, KB_DIR);
134
+ }
135
+ function ensureKBDir(cwd) {
136
+ const kbDir = getKBDir(cwd);
137
+ if (!fs.existsSync(kbDir)) {
138
+ fs.mkdirSync(kbDir, { recursive: true });
139
+ }
140
+ const ontoDir = path.join(kbDir, "ontology");
141
+ if (!fs.existsSync(ontoDir)) {
142
+ fs.mkdirSync(ontoDir, { recursive: true });
143
+ }
144
+ return kbDir;
145
+ }
146
+ function readSyncState(cwd) {
147
+ const statePath = path.join(getKBDir(cwd), SYNC_STATE_FILE);
148
+ if (fs.existsSync(statePath)) {
149
+ try {
150
+ return JSON.parse(fs.readFileSync(statePath, "utf-8"));
151
+ }
152
+ catch {
153
+ // corrupted file
154
+ }
155
+ }
156
+ return { botId: "", lastPull: null, lastPush: null, sharedCount: 0, botCount: 0 };
157
+ }
158
+ function writeSyncState(cwd, state) {
159
+ const kbDir = ensureKBDir(cwd);
160
+ fs.writeFileSync(path.join(kbDir, SYNC_STATE_FILE), JSON.stringify(state, null, 2));
161
+ }
162
+ function writeKBFile(cwd, filename, data) {
163
+ const kbDir = ensureKBDir(cwd);
164
+ fs.writeFileSync(path.join(kbDir, filename), JSON.stringify(data, null, 2));
165
+ }
166
+ function readKBFile(cwd, filename) {
167
+ const filePath = path.join(getKBDir(cwd), filename);
168
+ if (!fs.existsSync(filePath))
169
+ return [];
170
+ try {
171
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
172
+ }
173
+ catch {
174
+ return [];
175
+ }
176
+ }
177
+ // ============================================================
178
+ // Database Operations
179
+ // ============================================================
180
+ /**
181
+ * Get shared KB entries from semo.knowledge_base
182
+ */
183
+ async function kbPull(pool, botId, domain, cwd) {
184
+ const client = await pool.connect();
185
+ try {
186
+ // Shared KB
187
+ let sharedQuery = `
188
+ SELECT domain, key, content, metadata, created_by, version,
189
+ created_at::text, updated_at::text
190
+ FROM semo.knowledge_base
191
+ `;
192
+ const sharedParams = [];
193
+ if (domain) {
194
+ sharedQuery += " WHERE domain = $1";
195
+ sharedParams.push(domain);
196
+ }
197
+ sharedQuery += " ORDER BY domain, key";
198
+ const sharedResult = await client.query(sharedQuery, sharedParams);
199
+ const shared = sharedResult.rows;
200
+ // Bot-specific KB
201
+ let botQuery = `
202
+ SELECT bot_id, domain, key, content, metadata, version,
203
+ synced_at::text, created_at::text, updated_at::text
204
+ FROM semo.bot_knowledge
205
+ WHERE bot_id = $1
206
+ `;
207
+ const botParams = [botId];
208
+ if (domain) {
209
+ botQuery += " AND domain = $2";
210
+ botParams.push(domain);
211
+ }
212
+ botQuery += " ORDER BY domain, key";
213
+ const botResult = await client.query(botQuery, botParams);
214
+ const bot = botResult.rows;
215
+ // Write to local files if cwd provided
216
+ if (cwd) {
217
+ writeKBFile(cwd, "team.json", shared);
218
+ writeKBFile(cwd, "bot.json", bot);
219
+ const state = readSyncState(cwd);
220
+ state.botId = botId;
221
+ state.lastPull = new Date().toISOString();
222
+ state.sharedCount = shared.length;
223
+ state.botCount = bot.length;
224
+ writeSyncState(cwd, state);
225
+ }
226
+ return { shared, bot };
227
+ }
228
+ finally {
229
+ client.release();
230
+ }
231
+ }
232
+ /**
233
+ * Push local KB entries to database
234
+ */
235
+ async function kbPush(pool, botId, entries, target = "bot", cwd) {
236
+ const client = await pool.connect();
237
+ let upserted = 0;
238
+ const errors = [];
239
+ try {
240
+ await client.query("BEGIN");
241
+ for (const entry of entries) {
242
+ try {
243
+ // Generate embedding for content
244
+ const embedding = await generateEmbedding(`${entry.key}: ${entry.content}`);
245
+ const embeddingStr = embedding ? `[${embedding.join(",")}]` : null;
246
+ if (target === "shared") {
247
+ await client.query(`INSERT INTO semo.knowledge_base (domain, key, content, metadata, created_by, embedding)
248
+ VALUES ($1, $2, $3, $4, $5, $6::vector)
249
+ ON CONFLICT (domain, key) DO UPDATE SET
250
+ content = EXCLUDED.content,
251
+ metadata = EXCLUDED.metadata,
252
+ embedding = EXCLUDED.embedding`, [entry.domain, entry.key, entry.content, JSON.stringify(entry.metadata || {}), entry.created_by || botId, embeddingStr]);
253
+ }
254
+ else {
255
+ await client.query(`INSERT INTO semo.bot_knowledge (bot_id, domain, key, content, metadata, synced_at, embedding)
256
+ VALUES ($1, $2, $3, $4, $5, NOW(), $6::vector)
257
+ ON CONFLICT (bot_id, domain, key) DO UPDATE SET
258
+ content = EXCLUDED.content,
259
+ metadata = EXCLUDED.metadata,
260
+ synced_at = NOW(),
261
+ embedding = EXCLUDED.embedding`, [botId, entry.domain, entry.key, entry.content, JSON.stringify(entry.metadata || {}), embeddingStr]);
262
+ }
263
+ upserted++;
264
+ }
265
+ catch (err) {
266
+ errors.push(`${entry.domain}/${entry.key}: ${err}`);
267
+ }
268
+ }
269
+ await client.query("COMMIT");
270
+ if (cwd) {
271
+ const state = readSyncState(cwd);
272
+ state.botId = botId;
273
+ state.lastPush = new Date().toISOString();
274
+ writeSyncState(cwd, state);
275
+ }
276
+ }
277
+ catch (err) {
278
+ await client.query("ROLLBACK");
279
+ errors.push(`Transaction failed: ${err}`);
280
+ }
281
+ finally {
282
+ client.release();
283
+ }
284
+ return { upserted, errors };
285
+ }
286
+ /**
287
+ * Get KB status for a bot
288
+ */
289
+ async function kbStatus(pool, botId) {
290
+ const client = await pool.connect();
291
+ try {
292
+ // Shared KB stats
293
+ const sharedStats = await client.query(`
294
+ SELECT domain, COUNT(*)::int as count
295
+ FROM semo.knowledge_base
296
+ GROUP BY domain ORDER BY domain
297
+ `);
298
+ const sharedTotal = await client.query(`SELECT COUNT(*)::int as total FROM semo.knowledge_base`);
299
+ const sharedLastUpdated = await client.query(`SELECT MAX(updated_at)::text as last FROM semo.knowledge_base`);
300
+ const sharedDomains = {};
301
+ for (const row of sharedStats.rows) {
302
+ sharedDomains[row.domain] = row.count;
303
+ }
304
+ // Bot KB stats
305
+ const botStats = await client.query(`
306
+ SELECT domain, COUNT(*)::int as count
307
+ FROM semo.bot_knowledge WHERE bot_id = $1
308
+ GROUP BY domain ORDER BY domain
309
+ `, [botId]);
310
+ const botTotal = await client.query(`SELECT COUNT(*)::int as total FROM semo.bot_knowledge WHERE bot_id = $1`, [botId]);
311
+ const botLastUpdated = await client.query(`SELECT MAX(updated_at)::text as last FROM semo.bot_knowledge WHERE bot_id = $1`, [botId]);
312
+ const botLastSynced = await client.query(`SELECT MAX(synced_at)::text as last FROM semo.bot_knowledge WHERE bot_id = $1`, [botId]);
313
+ const botDomains = {};
314
+ for (const row of botStats.rows) {
315
+ botDomains[row.domain] = row.count;
316
+ }
317
+ return {
318
+ shared: {
319
+ total: sharedTotal.rows[0]?.total || 0,
320
+ domains: sharedDomains,
321
+ lastUpdated: sharedLastUpdated.rows[0]?.last || null,
322
+ },
323
+ bot: {
324
+ total: botTotal.rows[0]?.total || 0,
325
+ domains: botDomains,
326
+ lastUpdated: botLastUpdated.rows[0]?.last || null,
327
+ lastSynced: botLastSynced.rows[0]?.last || null,
328
+ },
329
+ };
330
+ }
331
+ finally {
332
+ client.release();
333
+ }
334
+ }
335
+ /**
336
+ * List KB entries with optional filters
337
+ */
338
+ async function kbList(pool, options) {
339
+ const client = await pool.connect();
340
+ const limit = options.limit || 50;
341
+ const offset = options.offset || 0;
342
+ try {
343
+ // Shared
344
+ let sharedQuery = "SELECT domain, key, content, metadata, created_by, version, updated_at::text FROM semo.knowledge_base";
345
+ const sharedParams = [];
346
+ let paramIdx = 1;
347
+ if (options.domain) {
348
+ sharedQuery += ` WHERE domain = $${paramIdx++}`;
349
+ sharedParams.push(options.domain);
350
+ }
351
+ sharedQuery += ` ORDER BY domain, key LIMIT $${paramIdx++} OFFSET $${paramIdx++}`;
352
+ sharedParams.push(limit, offset);
353
+ const sharedResult = await client.query(sharedQuery, sharedParams);
354
+ // Bot
355
+ let bot = [];
356
+ if (options.botId) {
357
+ let botQuery = "SELECT bot_id, domain, key, content, metadata, version, synced_at::text, updated_at::text FROM semo.bot_knowledge WHERE bot_id = $1";
358
+ const botParams = [options.botId];
359
+ let bParamIdx = 2;
360
+ if (options.domain) {
361
+ botQuery += ` AND domain = $${bParamIdx++}`;
362
+ botParams.push(options.domain);
363
+ }
364
+ botQuery += ` ORDER BY domain, key LIMIT $${bParamIdx++} OFFSET $${bParamIdx++}`;
365
+ botParams.push(limit, offset);
366
+ const botResult = await client.query(botQuery, botParams);
367
+ bot = botResult.rows;
368
+ }
369
+ return { shared: sharedResult.rows, bot };
370
+ }
371
+ finally {
372
+ client.release();
373
+ }
374
+ }
375
+ /**
376
+ * Diff local KB files against database
377
+ */
378
+ async function kbDiff(pool, botId, cwd) {
379
+ const localShared = readKBFile(cwd, "team.json");
380
+ const localBot = readKBFile(cwd, "bot.json");
381
+ const localAll = [...localShared, ...localBot];
382
+ const { shared: remoteShared, bot: remoteBot } = await kbPull(pool, botId);
383
+ const remoteAll = [...remoteShared, ...remoteBot];
384
+ const localMap = new Map(localAll.map(e => [`${e.domain}/${e.key}`, e]));
385
+ const remoteMap = new Map(remoteAll.map(e => [`${e.domain}/${e.key}`, e]));
386
+ const added = [];
387
+ const removed = [];
388
+ const modified = [];
389
+ let unchanged = 0;
390
+ // Remote entries not in local → added
391
+ for (const [k, v] of remoteMap) {
392
+ if (!localMap.has(k)) {
393
+ added.push(v);
394
+ }
395
+ }
396
+ // Local entries not in remote → removed
397
+ for (const [k, v] of localMap) {
398
+ if (!remoteMap.has(k)) {
399
+ removed.push(v);
400
+ }
401
+ }
402
+ // Both exist → check content
403
+ for (const [k, local] of localMap) {
404
+ const remote = remoteMap.get(k);
405
+ if (remote) {
406
+ if (local.content !== remote.content || JSON.stringify(local.metadata) !== JSON.stringify(remote.metadata)) {
407
+ modified.push({ local, remote });
408
+ }
409
+ else {
410
+ unchanged++;
411
+ }
412
+ }
413
+ }
414
+ return { added, removed, modified, unchanged };
415
+ }
416
+ /**
417
+ * Search KB — hybrid: vector similarity (if embedding available) + text fallback
418
+ */
419
+ async function kbSearch(pool, query, options) {
420
+ const client = await pool.connect();
421
+ const limit = options.limit || 10;
422
+ const mode = options.mode || "hybrid";
423
+ try {
424
+ let results = [];
425
+ // Try semantic search first (if mode allows and embedding API available)
426
+ if (mode !== "text") {
427
+ const queryEmbedding = await generateEmbedding(query);
428
+ if (queryEmbedding) {
429
+ const embeddingStr = `[${queryEmbedding.join(",")}]`;
430
+ // Vector search on shared KB
431
+ let sql = `
432
+ SELECT domain, key, content, metadata, created_by, version, updated_at::text,
433
+ 1 - (embedding <=> $1::vector) as score
434
+ FROM semo.knowledge_base
435
+ WHERE embedding IS NOT NULL
436
+ `;
437
+ const params = [embeddingStr];
438
+ let paramIdx = 2;
439
+ if (options.domain) {
440
+ sql += ` AND domain = $${paramIdx++}`;
441
+ params.push(options.domain);
442
+ }
443
+ sql += ` ORDER BY embedding <=> $1::vector LIMIT $${paramIdx++}`;
444
+ params.push(limit);
445
+ const sharedResult = await client.query(sql, params);
446
+ results = sharedResult.rows;
447
+ // Also search bot_knowledge if botId specified
448
+ if (options.botId) {
449
+ let botSql = `
450
+ SELECT bot_id, domain, key, content, metadata, version, updated_at::text,
451
+ 1 - (embedding <=> $1::vector) as score
452
+ FROM semo.bot_knowledge
453
+ WHERE bot_id = $2 AND embedding IS NOT NULL
454
+ `;
455
+ const botParams = [embeddingStr, options.botId];
456
+ let bIdx = 3;
457
+ if (options.domain) {
458
+ botSql += ` AND domain = $${bIdx++}`;
459
+ botParams.push(options.domain);
460
+ }
461
+ botSql += ` ORDER BY embedding <=> $1::vector LIMIT $${bIdx++}`;
462
+ botParams.push(limit);
463
+ const botResult = await client.query(botSql, botParams);
464
+ results = [...results, ...botResult.rows]
465
+ .sort((a, b) => (b.score || 0) - (a.score || 0))
466
+ .slice(0, limit);
467
+ }
468
+ // If we got results from semantic search and mode is not hybrid, return
469
+ if (results.length > 0 && mode === "semantic") {
470
+ return results;
471
+ }
472
+ }
473
+ }
474
+ // Text search (fallback or hybrid supplement)
475
+ if (mode !== "semantic" || results.length === 0) {
476
+ let textSql = `
477
+ SELECT domain, key, content, metadata, created_by, version, updated_at::text,
478
+ 0.0 as score
479
+ FROM semo.knowledge_base
480
+ WHERE content ILIKE $1 OR key ILIKE $1
481
+ `;
482
+ const textParams = [`%${query}%`];
483
+ let tIdx = 2;
484
+ if (options.domain) {
485
+ textSql += ` AND domain = $${tIdx++}`;
486
+ textParams.push(options.domain);
487
+ }
488
+ textSql += ` ORDER BY updated_at DESC LIMIT $${tIdx++}`;
489
+ textParams.push(limit);
490
+ const textResult = await client.query(textSql, textParams);
491
+ // Merge: deduplicate by domain/key, prefer semantic results
492
+ const seen = new Set(results.map((r) => `${r.domain}/${r.key}`));
493
+ for (const row of textResult.rows) {
494
+ const k = `${row.domain}/${row.key}`;
495
+ if (!seen.has(k)) {
496
+ results.push(row);
497
+ seen.add(k);
498
+ }
499
+ }
500
+ }
501
+ return results.slice(0, limit);
502
+ }
503
+ catch {
504
+ // Ultimate fallback: simple ILIKE
505
+ let sql = `
506
+ SELECT domain, key, content, metadata, created_by, version, updated_at::text
507
+ FROM semo.knowledge_base
508
+ WHERE content ILIKE $1 OR key ILIKE $1
509
+ `;
510
+ const params = [`%${query}%`];
511
+ let paramIdx = 2;
512
+ if (options.domain) {
513
+ sql += ` AND domain = $${paramIdx++}`;
514
+ params.push(options.domain);
515
+ }
516
+ sql += ` ORDER BY updated_at DESC LIMIT $${paramIdx++}`;
517
+ params.push(limit);
518
+ const result = await client.query(sql, params);
519
+ return result.rows;
520
+ }
521
+ finally {
522
+ client.release();
523
+ }
524
+ }
525
+ // ============================================================
526
+ // Ontology Operations
527
+ // ============================================================
528
+ /**
529
+ * List all ontology domains
530
+ */
531
+ async function ontoList(pool) {
532
+ const client = await pool.connect();
533
+ try {
534
+ const result = await client.query(`
535
+ SELECT domain, schema, description, version, updated_at::text
536
+ FROM semo.ontology ORDER BY domain
537
+ `);
538
+ return result.rows;
539
+ }
540
+ finally {
541
+ client.release();
542
+ }
543
+ }
544
+ /**
545
+ * Show ontology detail for a domain
546
+ */
547
+ async function ontoShow(pool, domain) {
548
+ const client = await pool.connect();
549
+ try {
550
+ const result = await client.query(`SELECT domain, schema, description, version, updated_at::text FROM semo.ontology WHERE domain = $1`, [domain]);
551
+ return result.rows[0] || null;
552
+ }
553
+ finally {
554
+ client.release();
555
+ }
556
+ }
557
+ /**
558
+ * Validate KB entries against ontology schema (basic JSON Schema validation)
559
+ */
560
+ async function ontoValidate(pool, domain, entries) {
561
+ const onto = await ontoShow(pool, domain);
562
+ if (!onto) {
563
+ return { valid: 0, invalid: [{ key: "*", errors: [`Ontology domain '${domain}' not found`] }] };
564
+ }
565
+ // If no entries provided, fetch from DB
566
+ if (!entries) {
567
+ const client = await pool.connect();
568
+ try {
569
+ const result = await client.query(`SELECT domain, key, content, metadata FROM semo.knowledge_base WHERE domain = $1`, [domain]);
570
+ entries = result.rows;
571
+ }
572
+ finally {
573
+ client.release();
574
+ }
575
+ }
576
+ const schema = onto.schema;
577
+ const required = schema.required || [];
578
+ let valid = 0;
579
+ const invalid = [];
580
+ for (const entry of entries) {
581
+ const errors = [];
582
+ // Check required fields
583
+ for (const field of required) {
584
+ if (!entry[field] && entry[field] !== "") {
585
+ errors.push(`Missing required field: ${field}`);
586
+ }
587
+ }
588
+ // Check metadata schema if defined
589
+ if (schema.properties?.metadata?.properties && entry.metadata) {
590
+ const metaSchema = schema.properties.metadata.properties;
591
+ for (const [propKey, propDef] of Object.entries(metaSchema)) {
592
+ const def = propDef;
593
+ const val = entry.metadata[propKey];
594
+ if (val !== undefined) {
595
+ if (def.enum && !def.enum.includes(val)) {
596
+ errors.push(`metadata.${propKey}: '${val}' not in allowed values [${def.enum.join(", ")}]`);
597
+ }
598
+ if (def.type === "string" && typeof val !== "string") {
599
+ errors.push(`metadata.${propKey}: expected string, got ${typeof val}`);
600
+ }
601
+ if (def.type === "array" && !Array.isArray(val)) {
602
+ errors.push(`metadata.${propKey}: expected array, got ${typeof val}`);
603
+ }
604
+ }
605
+ }
606
+ }
607
+ if (errors.length > 0) {
608
+ invalid.push({ key: entry.key, errors });
609
+ }
610
+ else {
611
+ valid++;
612
+ }
613
+ }
614
+ return { valid, invalid };
615
+ }
616
+ /**
617
+ * Write ontology schemas to local cache
618
+ */
619
+ async function ontoPullToLocal(pool, cwd) {
620
+ const domains = await ontoList(pool);
621
+ const kbDir = ensureKBDir(cwd);
622
+ const ontoDir = path.join(kbDir, "ontology");
623
+ for (const d of domains) {
624
+ fs.writeFileSync(path.join(ontoDir, `${d.domain}.json`), JSON.stringify(d, null, 2));
625
+ }
626
+ return domains.length;
627
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semo-cli",
3
- "version": "3.14.1",
3
+ "version": "4.0.0",
4
4
  "description": "SEMO CLI - AI Agent Orchestration Framework Installer",
5
5
  "main": "dist/index.js",
6
6
  "bin": {