@team-semicolon/semo-cli 4.7.4 → 4.8.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/index.js +101 -5
- package/dist/kb.d.ts +30 -0
- package/dist/kb.js +113 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1665,7 +1665,7 @@ kbCmd
|
|
|
1665
1665
|
});
|
|
1666
1666
|
kbCmd
|
|
1667
1667
|
.command("upsert <domain> <key> [sub_key]")
|
|
1668
|
-
.description("KB 항목 쓰기 (upsert) — 임베딩 자동 생성 + 스키마 검증")
|
|
1668
|
+
.description("KB 항목 쓰기 (upsert) — 임베딩 자동 생성 + 스키마 검증 (key는 kebab-case만 허용)")
|
|
1669
1669
|
.requiredOption("--content <text>", "항목 본문")
|
|
1670
1670
|
.option("--metadata <json>", "추가 메타데이터 (JSON 문자열)")
|
|
1671
1671
|
.option("--created-by <name>", "작성자 식별자", "semo-cli")
|
|
@@ -1702,13 +1702,66 @@ kbCmd
|
|
|
1702
1702
|
process.exit(1);
|
|
1703
1703
|
}
|
|
1704
1704
|
});
|
|
1705
|
+
kbCmd
|
|
1706
|
+
.command("delete <domain> <key> [sub_key]")
|
|
1707
|
+
.description("KB 항목 삭제 (domain + key + sub_key)")
|
|
1708
|
+
.option("--yes", "확인 프롬프트 건너뛰기 (크론/스크립트용)")
|
|
1709
|
+
.action(async (domain, key, subKey, options) => {
|
|
1710
|
+
try {
|
|
1711
|
+
const pool = (0, database_1.getPool)();
|
|
1712
|
+
// 삭제 전 항목 확인
|
|
1713
|
+
const entry = await (0, kb_1.kbGet)(pool, domain, key, subKey);
|
|
1714
|
+
if (!entry) {
|
|
1715
|
+
console.log(chalk_1.default.yellow(`\n 항목 없음: ${domain}/${key}${subKey ? '/' + subKey : ''}\n`));
|
|
1716
|
+
await (0, database_1.closeConnection)();
|
|
1717
|
+
process.exit(1);
|
|
1718
|
+
}
|
|
1719
|
+
// 확인 프롬프트 (--yes가 없으면)
|
|
1720
|
+
if (!options.yes) {
|
|
1721
|
+
const fullPath = `${domain}/${entry.key}${entry.sub_key ? '/' + entry.sub_key : ''}`;
|
|
1722
|
+
console.log(chalk_1.default.cyan(`\n📄 삭제 대상: ${fullPath}`));
|
|
1723
|
+
console.log(chalk_1.default.gray(` version: ${entry.version} | created_by: ${entry.created_by} | updated: ${entry.updated_at}`));
|
|
1724
|
+
console.log(chalk_1.default.gray(` content: ${entry.content.substring(0, 120)}${entry.content.length > 120 ? '...' : ''}\n`));
|
|
1725
|
+
const { createInterface } = await import("readline");
|
|
1726
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1727
|
+
const answer = await new Promise((resolve) => {
|
|
1728
|
+
rl.question(chalk_1.default.yellow(" 정말 삭제하시겠습니까? (y/N): "), resolve);
|
|
1729
|
+
});
|
|
1730
|
+
rl.close();
|
|
1731
|
+
if (answer.toLowerCase() !== "y") {
|
|
1732
|
+
console.log(chalk_1.default.gray(" 취소됨.\n"));
|
|
1733
|
+
await (0, database_1.closeConnection)();
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
const result = await (0, kb_1.kbDelete)(pool, domain, key, subKey);
|
|
1738
|
+
if (result.deleted) {
|
|
1739
|
+
const fullPath = `${domain}/${result.entry.key}${result.entry.sub_key ? '/' + result.entry.sub_key : ''}`;
|
|
1740
|
+
console.log(chalk_1.default.green(`✔ KB 삭제 완료: ${fullPath}`));
|
|
1741
|
+
}
|
|
1742
|
+
else {
|
|
1743
|
+
console.log(chalk_1.default.red(`✖ KB 삭제 실패: ${result.error}`));
|
|
1744
|
+
process.exit(1);
|
|
1745
|
+
}
|
|
1746
|
+
await (0, database_1.closeConnection)();
|
|
1747
|
+
}
|
|
1748
|
+
catch (err) {
|
|
1749
|
+
console.error(chalk_1.default.red(`삭제 실패: ${err}`));
|
|
1750
|
+
await (0, database_1.closeConnection)();
|
|
1751
|
+
process.exit(1);
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1705
1754
|
kbCmd
|
|
1706
1755
|
.command("ontology")
|
|
1707
1756
|
.description("온톨로지 조회 — 도메인/타입/스키마/라우팅 테이블")
|
|
1708
|
-
.option("--action <type>", "동작 (list|show|services|types|instances|schema|routing-table|register)", "list")
|
|
1757
|
+
.option("--action <type>", "동작 (list|show|services|types|instances|schema|routing-table|register|add-key|remove-key)", "list")
|
|
1709
1758
|
.option("--domain <name>", "action=show|register 시 도메인")
|
|
1710
|
-
.option("--type <name>", "action=schema|register 시 타입 키")
|
|
1711
|
-
.option("--
|
|
1759
|
+
.option("--type <name>", "action=schema|register|add-key|remove-key 시 타입 키")
|
|
1760
|
+
.option("--key <name>", "action=add-key|remove-key 시 스키마 키")
|
|
1761
|
+
.option("--key-type <type>", "action=add-key 시 키 유형 (singleton|collection)", "singleton")
|
|
1762
|
+
.option("--required", "action=add-key 시 필수 여부")
|
|
1763
|
+
.option("--hint <text>", "action=add-key 시 값 힌트")
|
|
1764
|
+
.option("--description <text>", "action=register|add-key 시 설명")
|
|
1712
1765
|
.option("--service <name>", "action=register 시 서비스 그룹")
|
|
1713
1766
|
.option("--tags <tags>", "action=register 시 태그 (쉼표 구분)")
|
|
1714
1767
|
.option("--no-init", "action=register 시 필수 KB entry 자동 생성 건너뛰기")
|
|
@@ -1896,8 +1949,51 @@ kbCmd
|
|
|
1896
1949
|
}
|
|
1897
1950
|
}
|
|
1898
1951
|
}
|
|
1952
|
+
else if (action === "add-key") {
|
|
1953
|
+
if (!options.type) {
|
|
1954
|
+
console.log(chalk_1.default.red("--type 옵션이 필요합니다. (예: --type service)"));
|
|
1955
|
+
process.exit(1);
|
|
1956
|
+
}
|
|
1957
|
+
if (!options.key) {
|
|
1958
|
+
console.log(chalk_1.default.red("--key 옵션이 필요합니다. (예: --key slack_channel)"));
|
|
1959
|
+
process.exit(1);
|
|
1960
|
+
}
|
|
1961
|
+
const result = await (0, kb_1.ontoAddKey)(pool, {
|
|
1962
|
+
type_key: options.type,
|
|
1963
|
+
scheme_key: options.key,
|
|
1964
|
+
description: options.description,
|
|
1965
|
+
key_type: options.keyType,
|
|
1966
|
+
required: options.required || false,
|
|
1967
|
+
value_hint: options.hint,
|
|
1968
|
+
});
|
|
1969
|
+
if (result.success) {
|
|
1970
|
+
console.log(chalk_1.default.green(`\n✅ 스키마 키 추가 완료: ${options.type}.${options.key} (${options.keyType})\n`));
|
|
1971
|
+
}
|
|
1972
|
+
else {
|
|
1973
|
+
console.log(chalk_1.default.red(`\n❌ 스키마 키 추가 실패: ${result.error}\n`));
|
|
1974
|
+
process.exit(1);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
else if (action === "remove-key") {
|
|
1978
|
+
if (!options.type) {
|
|
1979
|
+
console.log(chalk_1.default.red("--type 옵션이 필요합니다."));
|
|
1980
|
+
process.exit(1);
|
|
1981
|
+
}
|
|
1982
|
+
if (!options.key) {
|
|
1983
|
+
console.log(chalk_1.default.red("--key 옵션이 필요합니다."));
|
|
1984
|
+
process.exit(1);
|
|
1985
|
+
}
|
|
1986
|
+
const result = await (0, kb_1.ontoRemoveKey)(pool, options.type, options.key);
|
|
1987
|
+
if (result.success) {
|
|
1988
|
+
console.log(chalk_1.default.green(`\n✅ 스키마 키 삭제 완료: ${options.type}.${options.key}\n`));
|
|
1989
|
+
}
|
|
1990
|
+
else {
|
|
1991
|
+
console.log(chalk_1.default.red(`\n❌ 스키마 키 삭제 실패: ${result.error}\n`));
|
|
1992
|
+
process.exit(1);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1899
1995
|
else {
|
|
1900
|
-
console.log(chalk_1.default.red(`알 수 없는 action: '${action}'. 사용 가능: list, show, services, types, instances, schema, routing-table, register`));
|
|
1996
|
+
console.log(chalk_1.default.red(`알 수 없는 action: '${action}'. 사용 가능: list, show, services, types, instances, schema, routing-table, register, add-key, remove-key`));
|
|
1901
1997
|
process.exit(1);
|
|
1902
1998
|
}
|
|
1903
1999
|
await (0, database_1.closeConnection)();
|
package/dist/kb.d.ts
CHANGED
|
@@ -135,6 +135,15 @@ export declare function kbDigest(pool: Pool, since: string, domain?: string): Pr
|
|
|
135
135
|
* Get a single KB entry by domain + key + sub_key
|
|
136
136
|
*/
|
|
137
137
|
export declare function kbGet(pool: Pool, domain: string, rawKey: string, rawSubKey?: string): Promise<KBEntry | null>;
|
|
138
|
+
/**
|
|
139
|
+
* Delete a single KB entry by domain/key/sub_key.
|
|
140
|
+
* Returns the deleted entry content for confirmation, or null if not found.
|
|
141
|
+
*/
|
|
142
|
+
export declare function kbDelete(pool: Pool, domain: string, rawKey: string, rawSubKey?: string): Promise<{
|
|
143
|
+
deleted: boolean;
|
|
144
|
+
entry?: KBEntry;
|
|
145
|
+
error?: string;
|
|
146
|
+
}>;
|
|
138
147
|
/**
|
|
139
148
|
* Upsert a single KB entry with domain/key validation and embedding generation
|
|
140
149
|
*/
|
|
@@ -223,6 +232,27 @@ export interface OntoRegisterResult {
|
|
|
223
232
|
* 4. If init_required (default true), create KB entries for required keys in kb_type_schema
|
|
224
233
|
*/
|
|
225
234
|
export declare function ontoRegister(pool: Pool, opts: OntoRegisterOptions): Promise<OntoRegisterResult>;
|
|
235
|
+
/**
|
|
236
|
+
* Add a key to a type schema
|
|
237
|
+
*/
|
|
238
|
+
export declare function ontoAddKey(pool: Pool, opts: {
|
|
239
|
+
type_key: string;
|
|
240
|
+
scheme_key: string;
|
|
241
|
+
description?: string;
|
|
242
|
+
key_type?: "singleton" | "collection";
|
|
243
|
+
required?: boolean;
|
|
244
|
+
value_hint?: string;
|
|
245
|
+
}): Promise<{
|
|
246
|
+
success: boolean;
|
|
247
|
+
error?: string;
|
|
248
|
+
}>;
|
|
249
|
+
/**
|
|
250
|
+
* Remove a key from a type schema
|
|
251
|
+
*/
|
|
252
|
+
export declare function ontoRemoveKey(pool: Pool, typeKey: string, schemeKey: string): Promise<{
|
|
253
|
+
success: boolean;
|
|
254
|
+
error?: string;
|
|
255
|
+
}>;
|
|
226
256
|
/**
|
|
227
257
|
* Write ontology schemas to local cache
|
|
228
258
|
*/
|
package/dist/kb.js
CHANGED
|
@@ -55,12 +55,15 @@ exports.ontoListTypes = ontoListTypes;
|
|
|
55
55
|
exports.ontoValidate = ontoValidate;
|
|
56
56
|
exports.kbDigest = kbDigest;
|
|
57
57
|
exports.kbGet = kbGet;
|
|
58
|
+
exports.kbDelete = kbDelete;
|
|
58
59
|
exports.kbUpsert = kbUpsert;
|
|
59
60
|
exports.ontoListSchema = ontoListSchema;
|
|
60
61
|
exports.ontoRoutingTable = ontoRoutingTable;
|
|
61
62
|
exports.ontoListServices = ontoListServices;
|
|
62
63
|
exports.ontoListInstances = ontoListInstances;
|
|
63
64
|
exports.ontoRegister = ontoRegister;
|
|
65
|
+
exports.ontoAddKey = ontoAddKey;
|
|
66
|
+
exports.ontoRemoveKey = ontoRemoveKey;
|
|
64
67
|
exports.ontoPullToLocal = ontoPullToLocal;
|
|
65
68
|
const fs = __importStar(require("fs"));
|
|
66
69
|
const path = __importStar(require("path"));
|
|
@@ -683,6 +686,37 @@ async function kbGet(pool, domain, rawKey, rawSubKey) {
|
|
|
683
686
|
client.release();
|
|
684
687
|
}
|
|
685
688
|
}
|
|
689
|
+
/**
|
|
690
|
+
* Delete a single KB entry by domain/key/sub_key.
|
|
691
|
+
* Returns the deleted entry content for confirmation, or null if not found.
|
|
692
|
+
*/
|
|
693
|
+
async function kbDelete(pool, domain, rawKey, rawSubKey) {
|
|
694
|
+
let key;
|
|
695
|
+
let subKey;
|
|
696
|
+
if (rawSubKey !== undefined) {
|
|
697
|
+
key = rawKey;
|
|
698
|
+
subKey = rawSubKey;
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
const split = splitKey(rawKey);
|
|
702
|
+
key = split.key;
|
|
703
|
+
subKey = split.subKey;
|
|
704
|
+
}
|
|
705
|
+
const client = await pool.connect();
|
|
706
|
+
try {
|
|
707
|
+
const result = await client.query(`DELETE FROM semo.knowledge_base
|
|
708
|
+
WHERE domain = $1 AND key = $2 AND sub_key = $3
|
|
709
|
+
RETURNING domain, key, sub_key, content, metadata, created_by, version,
|
|
710
|
+
created_at::text, updated_at::text`, [domain, key, subKey]);
|
|
711
|
+
if (result.rows.length === 0) {
|
|
712
|
+
return { deleted: false, error: `항목 없음: ${domain}/${combineKey(key, subKey)}` };
|
|
713
|
+
}
|
|
714
|
+
return { deleted: true, entry: result.rows[0] };
|
|
715
|
+
}
|
|
716
|
+
finally {
|
|
717
|
+
client.release();
|
|
718
|
+
}
|
|
719
|
+
}
|
|
686
720
|
/**
|
|
687
721
|
* Upsert a single KB entry with domain/key validation and embedding generation
|
|
688
722
|
*/
|
|
@@ -714,6 +748,22 @@ async function kbUpsert(pool, entry) {
|
|
|
714
748
|
finally {
|
|
715
749
|
client.release();
|
|
716
750
|
}
|
|
751
|
+
// Naming convention check: kebab-case only
|
|
752
|
+
const warnings = [];
|
|
753
|
+
if (/_/.test(key)) {
|
|
754
|
+
const suggested = key.replace(/_/g, "-");
|
|
755
|
+
return {
|
|
756
|
+
success: false,
|
|
757
|
+
error: `키 '${key}'에 snake_case가 포함되어 있습니다. kebab-case를 사용하세요: '${suggested}'`,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
if (/_/.test(subKey)) {
|
|
761
|
+
const suggested = subKey.replace(/_/g, "-");
|
|
762
|
+
return {
|
|
763
|
+
success: false,
|
|
764
|
+
error: `sub_key '${subKey}'에 snake_case가 포함되어 있습니다. kebab-case를 사용하세요: '${suggested}'`,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
717
767
|
// Key validation against type schema
|
|
718
768
|
{
|
|
719
769
|
const schemaClient = await pool.connect();
|
|
@@ -960,6 +1010,69 @@ async function ontoRegister(pool, opts) {
|
|
|
960
1010
|
client.release();
|
|
961
1011
|
}
|
|
962
1012
|
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Add a key to a type schema
|
|
1015
|
+
*/
|
|
1016
|
+
async function ontoAddKey(pool, opts) {
|
|
1017
|
+
const client = await pool.connect();
|
|
1018
|
+
try {
|
|
1019
|
+
// Naming convention: kebab-case only
|
|
1020
|
+
if (/_/.test(opts.scheme_key)) {
|
|
1021
|
+
const suggested = opts.scheme_key.replace(/_/g, "-");
|
|
1022
|
+
return { success: false, error: `키 '${opts.scheme_key}'에 snake_case가 포함되어 있습니다. kebab-case를 사용하세요: '${suggested}'` };
|
|
1023
|
+
}
|
|
1024
|
+
// Check type exists
|
|
1025
|
+
const typeCheck = await client.query("SELECT DISTINCT type_key FROM semo.kb_type_schema WHERE type_key = $1", [opts.type_key]);
|
|
1026
|
+
if (typeCheck.rows.length === 0) {
|
|
1027
|
+
// Check if this type exists in ontology at all
|
|
1028
|
+
const ontoCheck = await client.query("SELECT DISTINCT entity_type FROM semo.ontology WHERE entity_type = $1", [opts.type_key]);
|
|
1029
|
+
if (ontoCheck.rows.length === 0) {
|
|
1030
|
+
return { success: false, error: `타입 '${opts.type_key}'이(가) 존재하지 않습니다.` };
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
// Check duplicate
|
|
1034
|
+
const dupCheck = await client.query("SELECT id FROM semo.kb_type_schema WHERE type_key = $1 AND scheme_key = $2", [opts.type_key, opts.scheme_key]);
|
|
1035
|
+
if (dupCheck.rows.length > 0) {
|
|
1036
|
+
return { success: false, error: `키 '${opts.scheme_key}'은(는) '${opts.type_key}' 타입에 이미 존재합니다.` };
|
|
1037
|
+
}
|
|
1038
|
+
// Get max sort_order
|
|
1039
|
+
const maxOrder = await client.query("SELECT COALESCE(MAX(sort_order), 0) + 10 as next_order FROM semo.kb_type_schema WHERE type_key = $1", [opts.type_key]);
|
|
1040
|
+
const sortOrder = maxOrder.rows[0].next_order;
|
|
1041
|
+
await client.query(`INSERT INTO semo.kb_type_schema (type_key, scheme_key, scheme_description, key_type, required, value_hint, sort_order)
|
|
1042
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`, [
|
|
1043
|
+
opts.type_key,
|
|
1044
|
+
opts.scheme_key,
|
|
1045
|
+
opts.description || opts.scheme_key,
|
|
1046
|
+
opts.key_type || "singleton",
|
|
1047
|
+
opts.required || false,
|
|
1048
|
+
opts.value_hint || null,
|
|
1049
|
+
sortOrder,
|
|
1050
|
+
]);
|
|
1051
|
+
return { success: true };
|
|
1052
|
+
}
|
|
1053
|
+
catch (err) {
|
|
1054
|
+
return { success: false, error: String(err) };
|
|
1055
|
+
}
|
|
1056
|
+
finally {
|
|
1057
|
+
client.release();
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Remove a key from a type schema
|
|
1062
|
+
*/
|
|
1063
|
+
async function ontoRemoveKey(pool, typeKey, schemeKey) {
|
|
1064
|
+
const client = await pool.connect();
|
|
1065
|
+
try {
|
|
1066
|
+
const result = await client.query("DELETE FROM semo.kb_type_schema WHERE type_key = $1 AND scheme_key = $2 RETURNING id", [typeKey, schemeKey]);
|
|
1067
|
+
if (result.rows.length === 0) {
|
|
1068
|
+
return { success: false, error: `키 '${schemeKey}'은(는) '${typeKey}' 타입에 존재하지 않습니다.` };
|
|
1069
|
+
}
|
|
1070
|
+
return { success: true };
|
|
1071
|
+
}
|
|
1072
|
+
finally {
|
|
1073
|
+
client.release();
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
963
1076
|
/**
|
|
964
1077
|
* Write ontology schemas to local cache
|
|
965
1078
|
*/
|