@team-semicolon/semo-cli 4.1.5 → 4.2.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.d.ts CHANGED
@@ -27,10 +27,6 @@ export interface KBEntry {
27
27
  created_at?: string;
28
28
  updated_at?: string;
29
29
  }
30
- export interface BotKBEntry extends KBEntry {
31
- bot_id: string;
32
- synced_at?: string;
33
- }
34
30
  export interface OntologyDomain {
35
31
  domain: string;
36
32
  schema: Record<string, unknown>;
@@ -44,69 +40,53 @@ export interface KBStatusInfo {
44
40
  domains: Record<string, number>;
45
41
  lastUpdated: string | null;
46
42
  };
47
- bot: {
48
- total: number;
49
- domains: Record<string, number>;
50
- lastUpdated: string | null;
51
- lastSynced: string | null;
52
- };
53
43
  }
54
- export interface KBDiffResult {
55
- added: KBEntry[];
56
- removed: KBEntry[];
57
- modified: Array<{
58
- local: KBEntry;
59
- remote: KBEntry;
60
- }>;
61
- unchanged: number;
44
+ export interface KBDigestEntry {
45
+ domain: string;
46
+ key: string;
47
+ content: string;
48
+ version: number;
49
+ change_type: 'new' | 'updated';
50
+ updated_at: string;
51
+ }
52
+ export interface KBDigestResult {
53
+ changes: KBDigestEntry[];
54
+ since: string;
55
+ generatedAt: string;
62
56
  }
63
57
  export interface SyncState {
64
- botId: string;
65
58
  lastPull: string | null;
66
59
  lastPush: string | null;
67
60
  sharedCount: number;
68
- botCount: number;
69
61
  }
70
62
  /**
71
- * Get shared KB entries from semo.knowledge_base
63
+ * Pull KB entries from semo.knowledge_base to local .kb/
72
64
  */
73
- export declare function kbPull(pool: Pool, botId: string, domain?: string, cwd?: string): Promise<{
74
- shared: KBEntry[];
75
- bot: BotKBEntry[];
76
- }>;
65
+ export declare function kbPull(pool: Pool, domain?: string, cwd?: string): Promise<KBEntry[]>;
77
66
  /**
78
- * Push local KB entries to database
67
+ * Push local KB entries to database (knowledge_base only)
79
68
  */
80
- export declare function kbPush(pool: Pool, botId: string, entries: KBEntry[], target?: "shared" | "bot", cwd?: string): Promise<{
69
+ export declare function kbPush(pool: Pool, entries: KBEntry[], createdBy?: string, cwd?: string): Promise<{
81
70
  upserted: number;
82
71
  errors: string[];
83
72
  }>;
84
73
  /**
85
- * Get KB status for a bot
74
+ * Get KB status
86
75
  */
87
- export declare function kbStatus(pool: Pool, botId: string): Promise<KBStatusInfo>;
76
+ export declare function kbStatus(pool: Pool): Promise<KBStatusInfo>;
88
77
  /**
89
78
  * List KB entries with optional filters
90
79
  */
91
80
  export declare function kbList(pool: Pool, options: {
92
81
  domain?: string;
93
- botId?: string;
94
82
  limit?: number;
95
83
  offset?: number;
96
- }): Promise<{
97
- shared: KBEntry[];
98
- bot: BotKBEntry[];
99
- }>;
100
- /**
101
- * Diff local KB files against database
102
- */
103
- export declare function kbDiff(pool: Pool, botId: string, cwd: string): Promise<KBDiffResult>;
84
+ }): Promise<KBEntry[]>;
104
85
  /**
105
86
  * Search KB — hybrid: vector similarity (if embedding available) + text fallback
106
87
  */
107
88
  export declare function kbSearch(pool: Pool, query: string, options: {
108
89
  domain?: string;
109
- botId?: string;
110
90
  limit?: number;
111
91
  mode?: "semantic" | "text" | "hybrid";
112
92
  }): Promise<KBEntry[]>;
@@ -128,6 +108,11 @@ export declare function ontoValidate(pool: Pool, domain: string, entries?: KBEnt
128
108
  errors: string[];
129
109
  }>;
130
110
  }>;
111
+ /**
112
+ * Generate KB change digest based on a since timestamp.
113
+ * Returns all KB changes since the given ISO timestamp.
114
+ */
115
+ export declare function kbDigest(pool: Pool, since: string, domain?: string): Promise<KBDigestResult>;
131
116
  /**
132
117
  * Write ontology schemas to local cache
133
118
  */
package/dist/kb.js CHANGED
@@ -48,11 +48,11 @@ exports.kbPull = kbPull;
48
48
  exports.kbPush = kbPush;
49
49
  exports.kbStatus = kbStatus;
50
50
  exports.kbList = kbList;
51
- exports.kbDiff = kbDiff;
52
51
  exports.kbSearch = kbSearch;
53
52
  exports.ontoList = ontoList;
54
53
  exports.ontoShow = ontoShow;
55
54
  exports.ontoValidate = ontoValidate;
55
+ exports.kbDigest = kbDigest;
56
56
  exports.ontoPullToLocal = ontoPullToLocal;
57
57
  const fs = __importStar(require("fs"));
58
58
  const path = __importStar(require("path"));
@@ -153,7 +153,7 @@ function readSyncState(cwd) {
153
153
  // corrupted file
154
154
  }
155
155
  }
156
- return { botId: "", lastPull: null, lastPush: null, sharedCount: 0, botCount: 0 };
156
+ return { lastPull: null, lastPush: null, sharedCount: 0 };
157
157
  }
158
158
  function writeSyncState(cwd, state) {
159
159
  const kbDir = ensureKBDir(cwd);
@@ -178,88 +178,75 @@ function readKBFile(cwd, filename) {
178
178
  // Database Operations
179
179
  // ============================================================
180
180
  /**
181
- * Get shared KB entries from semo.knowledge_base
181
+ * Pull KB entries from semo.knowledge_base to local .kb/
182
182
  */
183
- async function kbPull(pool, botId, domain, cwd) {
183
+ async function kbPull(pool, domain, cwd) {
184
184
  const client = await pool.connect();
185
185
  try {
186
- // Shared KB
187
- let sharedQuery = `
186
+ let query = `
188
187
  SELECT domain, key, content, metadata, created_by, version,
189
188
  created_at::text, updated_at::text
190
189
  FROM semo.knowledge_base
191
190
  `;
192
- const sharedParams = [];
191
+ const params = [];
193
192
  if (domain) {
194
- sharedQuery += " WHERE domain = $1";
195
- sharedParams.push(domain);
193
+ query += " WHERE domain = $1";
194
+ params.push(domain);
196
195
  }
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
196
+ query += " ORDER BY domain, key";
197
+ const result = await client.query(query, params);
198
+ const entries = result.rows;
216
199
  if (cwd) {
217
- writeKBFile(cwd, "team.json", shared);
218
- writeKBFile(cwd, "bot.json", bot);
200
+ writeKBFile(cwd, "team.json", entries);
219
201
  const state = readSyncState(cwd);
220
- state.botId = botId;
221
202
  state.lastPull = new Date().toISOString();
222
- state.sharedCount = shared.length;
223
- state.botCount = bot.length;
203
+ state.sharedCount = entries.length;
224
204
  writeSyncState(cwd, state);
225
205
  }
226
- return { shared, bot };
206
+ return entries;
227
207
  }
228
208
  finally {
229
209
  client.release();
230
210
  }
231
211
  }
232
212
  /**
233
- * Push local KB entries to database
213
+ * Push local KB entries to database (knowledge_base only)
234
214
  */
235
- async function kbPush(pool, botId, entries, target = "bot", cwd) {
215
+ async function kbPush(pool, entries, createdBy, cwd) {
236
216
  const client = await pool.connect();
237
217
  let upserted = 0;
238
218
  const errors = [];
239
219
  try {
240
- await client.query("BEGIN");
220
+ // Domain validation: check all domains against ontology before transaction
221
+ const ontologyResult = await client.query("SELECT domain FROM semo.ontology");
222
+ const knownDomains = new Set(ontologyResult.rows.map((r) => r.domain));
223
+ const invalidEntries = [];
224
+ const validEntries = [];
241
225
  for (const entry of entries) {
226
+ if (knownDomains.has(entry.domain)) {
227
+ validEntries.push(entry);
228
+ }
229
+ else {
230
+ invalidEntries.push(`${entry.domain}/${entry.key}: 미등록 도메인 '${entry.domain}'`);
231
+ }
232
+ }
233
+ if (invalidEntries.length > 0) {
234
+ errors.push(...invalidEntries);
235
+ }
236
+ await client.query("BEGIN");
237
+ const texts = validEntries.map(e => `${e.key}: ${e.content}`);
238
+ const embeddings = await generateEmbeddings(texts);
239
+ for (let i = 0; i < validEntries.length; i++) {
240
+ const entry = validEntries[i];
242
241
  try {
243
- // Generate embedding for content
244
- const embedding = await generateEmbedding(`${entry.key}: ${entry.content}`);
242
+ const embedding = embeddings[i];
245
243
  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
- }
244
+ await client.query(`INSERT INTO semo.knowledge_base (domain, key, content, metadata, created_by, embedding)
245
+ VALUES ($1, $2, $3, $4, $5, $6::vector)
246
+ ON CONFLICT (domain, key) DO UPDATE SET
247
+ content = EXCLUDED.content,
248
+ metadata = EXCLUDED.metadata,
249
+ embedding = EXCLUDED.embedding`, [entry.domain, entry.key, entry.content, JSON.stringify(entry.metadata || {}), entry.created_by || createdBy || "unknown", embeddingStr]);
263
250
  upserted++;
264
251
  }
265
252
  catch (err) {
@@ -269,7 +256,6 @@ async function kbPush(pool, botId, entries, target = "bot", cwd) {
269
256
  await client.query("COMMIT");
270
257
  if (cwd) {
271
258
  const state = readSyncState(cwd);
272
- state.botId = botId;
273
259
  state.lastPush = new Date().toISOString();
274
260
  writeSyncState(cwd, state);
275
261
  }
@@ -284,12 +270,11 @@ async function kbPush(pool, botId, entries, target = "bot", cwd) {
284
270
  return { upserted, errors };
285
271
  }
286
272
  /**
287
- * Get KB status for a bot
273
+ * Get KB status
288
274
  */
289
- async function kbStatus(pool, botId) {
275
+ async function kbStatus(pool) {
290
276
  const client = await pool.connect();
291
277
  try {
292
- // Shared KB stats
293
278
  const sharedStats = await client.query(`
294
279
  SELECT domain, COUNT(*)::int as count
295
280
  FROM semo.knowledge_base
@@ -301,31 +286,12 @@ async function kbStatus(pool, botId) {
301
286
  for (const row of sharedStats.rows) {
302
287
  sharedDomains[row.domain] = row.count;
303
288
  }
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
289
  return {
318
290
  shared: {
319
291
  total: sharedTotal.rows[0]?.total || 0,
320
292
  domains: sharedDomains,
321
293
  lastUpdated: sharedLastUpdated.rows[0]?.last || null,
322
294
  },
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
295
  };
330
296
  }
331
297
  finally {
@@ -340,79 +306,22 @@ async function kbList(pool, options) {
340
306
  const limit = options.limit || 50;
341
307
  const offset = options.offset || 0;
342
308
  try {
343
- // Shared
344
- let sharedQuery = "SELECT domain, key, content, metadata, created_by, version, updated_at::text FROM semo.knowledge_base";
345
- const sharedParams = [];
309
+ let query = "SELECT domain, key, content, metadata, created_by, version, updated_at::text FROM semo.knowledge_base";
310
+ const params = [];
346
311
  let paramIdx = 1;
347
312
  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;
313
+ query += ` WHERE domain = $${paramIdx++}`;
314
+ params.push(options.domain);
368
315
  }
369
- return { shared: sharedResult.rows, bot };
316
+ query += ` ORDER BY domain, key LIMIT $${paramIdx++} OFFSET $${paramIdx++}`;
317
+ params.push(limit, offset);
318
+ const result = await client.query(query, params);
319
+ return result.rows;
370
320
  }
371
321
  finally {
372
322
  client.release();
373
323
  }
374
324
  }
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
325
  /**
417
326
  * Search KB — hybrid: vector similarity (if embedding available) + text fallback
418
327
  */
@@ -444,27 +353,6 @@ async function kbSearch(pool, query, options) {
444
353
  params.push(limit);
445
354
  const sharedResult = await client.query(sql, params);
446
355
  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
356
  // If we got results from semantic search and mode is not hybrid, return
469
357
  if (results.length > 0 && mode === "semantic") {
470
358
  return results;
@@ -472,31 +360,58 @@ async function kbSearch(pool, query, options) {
472
360
  }
473
361
  }
474
362
  // Text search (fallback or hybrid supplement)
363
+ // Split query into tokens and match ANY token via ILIKE (Korean-friendly)
475
364
  if (mode !== "semantic" || results.length === 0) {
365
+ const tokens = query.split(/\s+/).filter(t => t.length >= 2);
366
+ const textParams = [];
367
+ let tIdx = 1;
368
+ // Build per-token ILIKE conditions + count matching tokens for scoring
369
+ const tokenConditions = tokens.map(token => {
370
+ textParams.push(`%${token}%`);
371
+ return `(CASE WHEN content ILIKE $${tIdx} OR key ILIKE $${tIdx++} THEN 1 ELSE 0 END)`;
372
+ });
373
+ // Score = 0.7 base + 0.15 * (matched_tokens / total_tokens), capped at 0.95
374
+ const matchCountExpr = tokenConditions.length > 0
375
+ ? tokenConditions.join(" + ")
376
+ : "0";
377
+ const scoreExpr = `LEAST(0.95, 0.7 + 0.15 * (${matchCountExpr})::float / ${Math.max(tokens.length, 1)})`;
378
+ // WHERE: any token matches
379
+ const whereTokens = tokens.map((_, i) => `(content ILIKE $${i + 1} OR key ILIKE $${i + 1})`);
380
+ const whereClause = whereTokens.length > 0
381
+ ? whereTokens.join(" OR ")
382
+ : "FALSE";
476
383
  let textSql = `
477
384
  SELECT domain, key, content, metadata, created_by, version, updated_at::text,
478
- 0.0 as score
385
+ ${scoreExpr} as score
479
386
  FROM semo.knowledge_base
480
- WHERE content ILIKE $1 OR key ILIKE $1
387
+ WHERE ${whereClause}
481
388
  `;
482
- const textParams = [`%${query}%`];
483
- let tIdx = 2;
484
389
  if (options.domain) {
485
390
  textSql += ` AND domain = $${tIdx++}`;
486
391
  textParams.push(options.domain);
487
392
  }
488
- textSql += ` ORDER BY updated_at DESC LIMIT $${tIdx++}`;
393
+ textSql += ` ORDER BY score DESC, updated_at DESC LIMIT $${tIdx++}`;
489
394
  textParams.push(limit);
490
395
  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}`));
396
+ // Merge: text matches get priority score (0.85) for exact keyword hits
397
+ // Deduplicate by domain/key; if already in semantic results, boost its score
398
+ const resultMap = new Map();
399
+ for (const r of results) {
400
+ resultMap.set(`${r.domain}/${r.key}`, r);
401
+ }
493
402
  for (const row of textResult.rows) {
494
403
  const k = `${row.domain}/${row.key}`;
495
- if (!seen.has(k)) {
496
- results.push(row);
497
- seen.add(k);
404
+ const existing = resultMap.get(k);
405
+ if (existing) {
406
+ // Boost: semantic match + text match = highest relevance
407
+ existing.score = Math.max(Number(existing.score), 0.85);
408
+ }
409
+ else {
410
+ resultMap.set(k, row);
498
411
  }
499
412
  }
413
+ // Sort by score descending
414
+ results = Array.from(resultMap.values()).sort((a, b) => Number(b.score) - Number(a.score));
500
415
  }
501
416
  return results.slice(0, limit);
502
417
  }
@@ -613,6 +528,37 @@ async function ontoValidate(pool, domain, entries) {
613
528
  }
614
529
  return { valid, invalid };
615
530
  }
531
+ // ============================================================
532
+ // KB Digest
533
+ // ============================================================
534
+ /**
535
+ * Generate KB change digest based on a since timestamp.
536
+ * Returns all KB changes since the given ISO timestamp.
537
+ */
538
+ async function kbDigest(pool, since, domain) {
539
+ const client = await pool.connect();
540
+ const generatedAt = new Date().toISOString();
541
+ try {
542
+ let sql = `
543
+ SELECT domain, key, content, version, updated_at::text,
544
+ CASE WHEN created_at > $1 THEN 'new' ELSE 'updated' END as change_type
545
+ FROM semo.knowledge_base
546
+ WHERE updated_at > $1 OR created_at > $1
547
+ `;
548
+ const params = [since];
549
+ let paramIdx = 2;
550
+ if (domain) {
551
+ sql += ` AND domain = $${paramIdx++}`;
552
+ params.push(domain);
553
+ }
554
+ sql += ` ORDER BY updated_at DESC LIMIT 100`;
555
+ const result = await client.query(sql, params);
556
+ return { changes: result.rows, since, generatedAt };
557
+ }
558
+ finally {
559
+ client.release();
560
+ }
561
+ }
616
562
  /**
617
563
  * Write ontology schemas to local cache
618
564
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semo-cli",
3
- "version": "4.1.5",
3
+ "version": "4.2.0",
4
4
  "description": "SEMO CLI - AI Agent Orchestration Framework Installer",
5
5
  "main": "dist/index.js",
6
6
  "bin": {