@team-semicolon/semo-cli 4.7.3 → 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 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,12 +1702,69 @@ 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)", "list")
1709
- .option("--domain <name>", "action=show 시 도메인")
1710
- .option("--type <name>", "action=schema 시 타입 키")
1757
+ .option("--action <type>", "동작 (list|show|services|types|instances|schema|routing-table|register|add-key|remove-key)", "list")
1758
+ .option("--domain <name>", "action=show|register 시 도메인")
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 시 설명")
1765
+ .option("--service <name>", "action=register 시 서비스 그룹")
1766
+ .option("--tags <tags>", "action=register 시 태그 (쉼표 구분)")
1767
+ .option("--no-init", "action=register 시 필수 KB entry 자동 생성 건너뛰기")
1711
1768
  .option("--format <type>", "출력 형식 (json|table)", "table")
1712
1769
  .action(async (options) => {
1713
1770
  try {
@@ -1852,8 +1909,91 @@ kbCmd
1852
1909
  console.log();
1853
1910
  }
1854
1911
  }
1912
+ else if (action === "register") {
1913
+ if (!options.domain) {
1914
+ console.log(chalk_1.default.red("--domain 옵션이 필요합니다."));
1915
+ process.exit(1);
1916
+ }
1917
+ if (!options.type) {
1918
+ console.log(chalk_1.default.red("--type 옵션이 필요합니다. (예: --type service, --type team, --type person)"));
1919
+ const types = await (0, kb_1.ontoListTypes)(pool);
1920
+ console.log(chalk_1.default.gray(`사용 가능한 타입: ${types.map(t => t.type_key).join(", ")}`));
1921
+ process.exit(1);
1922
+ }
1923
+ const tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : undefined;
1924
+ const result = await (0, kb_1.ontoRegister)(pool, {
1925
+ domain: options.domain,
1926
+ entity_type: options.type,
1927
+ description: options.description,
1928
+ service: options.service,
1929
+ tags,
1930
+ init_required: options.init !== false,
1931
+ });
1932
+ if (options.format === "json") {
1933
+ console.log(JSON.stringify(result, null, 2));
1934
+ }
1935
+ else {
1936
+ if (result.success) {
1937
+ console.log(chalk_1.default.green(`\n✅ 도메인 '${options.domain}' 등록 완료 (타입: ${options.type})`));
1938
+ if (result.created_entries && result.created_entries.length > 0) {
1939
+ console.log(chalk_1.default.gray(` 초기 KB entry ${result.created_entries.length}건 생성:`));
1940
+ for (const e of result.created_entries) {
1941
+ console.log(chalk_1.default.gray(` - ${e.key}`));
1942
+ }
1943
+ }
1944
+ console.log(chalk_1.default.gray(`\n 다음 단계: semo kb upsert ${options.domain} <key> --content "..." 으로 데이터 입력\n`));
1945
+ }
1946
+ else {
1947
+ console.log(chalk_1.default.red(`\n❌ 등록 실패: ${result.error}\n`));
1948
+ process.exit(1);
1949
+ }
1950
+ }
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
+ }
1855
1995
  else {
1856
- console.log(chalk_1.default.red(`알 수 없는 action: '${action}'. 사용 가능: list, show, services, types, instances, schema, routing-table`));
1996
+ console.log(chalk_1.default.red(`알 수 없는 action: '${action}'. 사용 가능: list, show, services, types, instances, schema, routing-table, register, add-key, remove-key`));
1857
1997
  process.exit(1);
1858
1998
  }
1859
1999
  await (0, database_1.closeConnection)();
@@ -2033,6 +2173,54 @@ ontoCmd
2033
2173
  process.exit(1);
2034
2174
  }
2035
2175
  });
2176
+ ontoCmd
2177
+ .command("register <domain>")
2178
+ .description("새 온톨로지 도메인 등록")
2179
+ .requiredOption("--type <type>", "엔티티 타입 (예: service, team, person, bot)")
2180
+ .option("--description <text>", "도메인 설명")
2181
+ .option("--service <name>", "서비스 그룹")
2182
+ .option("--tags <tags>", "태그 (쉼표 구분)")
2183
+ .option("--no-init", "필수 KB entry 자동 생성 건너뛰기")
2184
+ .option("--format <type>", "출력 형식 (json|table)", "table")
2185
+ .action(async (domain, options) => {
2186
+ try {
2187
+ const pool = (0, database_1.getPool)();
2188
+ const tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : undefined;
2189
+ const result = await (0, kb_1.ontoRegister)(pool, {
2190
+ domain,
2191
+ entity_type: options.type,
2192
+ description: options.description,
2193
+ service: options.service,
2194
+ tags,
2195
+ init_required: options.init !== false,
2196
+ });
2197
+ if (options.format === "json") {
2198
+ console.log(JSON.stringify(result, null, 2));
2199
+ }
2200
+ else {
2201
+ if (result.success) {
2202
+ console.log(chalk_1.default.green(`\n✅ 도메인 '${domain}' 등록 완료 (타입: ${options.type})`));
2203
+ if (result.created_entries && result.created_entries.length > 0) {
2204
+ console.log(chalk_1.default.gray(` 초기 KB entry ${result.created_entries.length}건 생성:`));
2205
+ for (const e of result.created_entries) {
2206
+ console.log(chalk_1.default.gray(` - ${e.key}`));
2207
+ }
2208
+ }
2209
+ console.log(chalk_1.default.gray(`\n 다음 단계: semo kb upsert ${domain} <key> --content "..." 으로 데이터 입력\n`));
2210
+ }
2211
+ else {
2212
+ console.log(chalk_1.default.red(`\n❌ 등록 실패: ${result.error}\n`));
2213
+ process.exit(1);
2214
+ }
2215
+ }
2216
+ await (0, database_1.closeConnection)();
2217
+ }
2218
+ catch (err) {
2219
+ console.error(chalk_1.default.red(`등록 실패: ${err}`));
2220
+ await (0, database_1.closeConnection)();
2221
+ process.exit(1);
2222
+ }
2223
+ });
2036
2224
  // === 신규 v4 커맨드 그룹 등록 ===
2037
2225
  (0, context_1.registerContextCommands)(program);
2038
2226
  (0, bots_1.registerBotsCommands)(program);
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
  */
@@ -198,6 +207,52 @@ export declare function ontoListServices(pool: Pool): Promise<ServiceInfo[]>;
198
207
  * List service instances (entity_type = 'service')
199
208
  */
200
209
  export declare function ontoListInstances(pool: Pool): Promise<ServiceInstance[]>;
210
+ export interface OntoRegisterOptions {
211
+ domain: string;
212
+ entity_type: string;
213
+ description?: string;
214
+ service?: string;
215
+ tags?: string[];
216
+ init_required?: boolean;
217
+ }
218
+ export interface OntoRegisterResult {
219
+ success: boolean;
220
+ error?: string;
221
+ created_entries?: Array<{
222
+ key: string;
223
+ sub_key: string;
224
+ }>;
225
+ }
226
+ /**
227
+ * Register a new ontology domain with optional initial required KB entries.
228
+ *
229
+ * 1. Validate entity_type exists in ontology_types
230
+ * 2. Check domain doesn't already exist
231
+ * 3. INSERT into semo.ontology
232
+ * 4. If init_required (default true), create KB entries for required keys in kb_type_schema
233
+ */
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
+ }>;
201
256
  /**
202
257
  * Write ontology schemas to local cache
203
258
  */
package/dist/kb.js CHANGED
@@ -55,11 +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;
64
+ exports.ontoRegister = ontoRegister;
65
+ exports.ontoAddKey = ontoAddKey;
66
+ exports.ontoRemoveKey = ontoRemoveKey;
63
67
  exports.ontoPullToLocal = ontoPullToLocal;
64
68
  const fs = __importStar(require("fs"));
65
69
  const path = __importStar(require("path"));
@@ -682,6 +686,37 @@ async function kbGet(pool, domain, rawKey, rawSubKey) {
682
686
  client.release();
683
687
  }
684
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
+ }
685
720
  /**
686
721
  * Upsert a single KB entry with domain/key validation and embedding generation
687
722
  */
@@ -713,6 +748,22 @@ async function kbUpsert(pool, entry) {
713
748
  finally {
714
749
  client.release();
715
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
+ }
716
767
  // Key validation against type schema
717
768
  {
718
769
  const schemaClient = await pool.connect();
@@ -886,6 +937,142 @@ async function ontoListInstances(pool) {
886
937
  client.release();
887
938
  }
888
939
  }
940
+ /**
941
+ * Register a new ontology domain with optional initial required KB entries.
942
+ *
943
+ * 1. Validate entity_type exists in ontology_types
944
+ * 2. Check domain doesn't already exist
945
+ * 3. INSERT into semo.ontology
946
+ * 4. If init_required (default true), create KB entries for required keys in kb_type_schema
947
+ */
948
+ async function ontoRegister(pool, opts) {
949
+ const client = await pool.connect();
950
+ try {
951
+ // 1. Validate entity_type
952
+ const typeCheck = await client.query("SELECT type_key FROM semo.ontology_types WHERE type_key = $1", [opts.entity_type]);
953
+ if (typeCheck.rows.length === 0) {
954
+ const known = await client.query("SELECT type_key FROM semo.ontology_types ORDER BY type_key");
955
+ const knownTypes = known.rows.map((r) => r.type_key);
956
+ return {
957
+ success: false,
958
+ error: `타입 '${opts.entity_type}'은(는) 존재하지 않습니다. 사용 가능한 타입: [${knownTypes.join(", ")}]`,
959
+ };
960
+ }
961
+ // 2. Check domain doesn't already exist
962
+ const existCheck = await client.query("SELECT domain FROM semo.ontology WHERE domain = $1", [opts.domain]);
963
+ if (existCheck.rows.length > 0) {
964
+ return { success: false, error: `도메인 '${opts.domain}'은(는) 이미 등록되어 있습니다.` };
965
+ }
966
+ // 3. INSERT into ontology
967
+ await client.query(`INSERT INTO semo.ontology (domain, entity_type, description, service, tags, schema)
968
+ VALUES ($1, $2, $3, $4, $5, $6)`, [
969
+ opts.domain,
970
+ opts.entity_type,
971
+ opts.description || null,
972
+ opts.service || "_global",
973
+ opts.tags || [opts.entity_type],
974
+ JSON.stringify({}),
975
+ ]);
976
+ // 4. Create required KB entries
977
+ const createdEntries = [];
978
+ const initRequired = opts.init_required !== false;
979
+ if (initRequired) {
980
+ const schemaResult = await client.query(`SELECT scheme_key, scheme_description, COALESCE(key_type, 'singleton') as key_type, value_hint
981
+ FROM semo.kb_type_schema
982
+ WHERE type_key = $1 AND required = true
983
+ ORDER BY sort_order`, [opts.entity_type]);
984
+ for (const s of schemaResult.rows) {
985
+ if (s.key_type === "collection")
986
+ continue; // collection은 sub_key가 필요하므로 스킵
987
+ const placeholder = s.value_hint
988
+ ? `(미입력 — hint: ${s.value_hint})`
989
+ : `(미입력)`;
990
+ const text = `${s.scheme_key}: ${placeholder}`;
991
+ const embedding = await generateEmbedding(text);
992
+ const embeddingStr = embedding ? `[${embedding.join(",")}]` : null;
993
+ try {
994
+ await client.query(`INSERT INTO semo.knowledge_base (domain, key, sub_key, content, metadata, created_by, embedding)
995
+ VALUES ($1, $2, '', $3, '{}', 'semo-cli:onto-register', $4::vector)
996
+ ON CONFLICT (domain, key, sub_key) DO NOTHING`, [opts.domain, s.scheme_key, placeholder, embeddingStr]);
997
+ createdEntries.push({ key: s.scheme_key, sub_key: "" });
998
+ }
999
+ catch {
1000
+ // 개별 entry 실패는 무시 — 도메인 등록 자체는 성공
1001
+ }
1002
+ }
1003
+ }
1004
+ return { success: true, created_entries: createdEntries };
1005
+ }
1006
+ catch (err) {
1007
+ return { success: false, error: String(err) };
1008
+ }
1009
+ finally {
1010
+ client.release();
1011
+ }
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
+ }
889
1076
  /**
890
1077
  * Write ontology schemas to local cache
891
1078
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semo-cli",
3
- "version": "4.7.3",
3
+ "version": "4.8.0",
4
4
  "description": "SEMO CLI - AI Agent Orchestration Framework Installer",
5
5
  "main": "dist/index.js",
6
6
  "bin": {