@team-semicolon/semo-cli 4.9.0 → 4.11.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/database.js CHANGED
@@ -68,11 +68,8 @@ const env_parser_1 = require("./env-parser");
68
68
  // ~/.claude/semo/.env 자동 로드 — LaunchAgent / Claude Code 앱 / cron 등
69
69
  // 인터랙티브 쉘이 아닌 환경에서 환경변수를 공급한다.
70
70
  // 이미 설정된 환경변수는 덮어쓰지 않는다 (env var > file).
71
- // v4.5.0: ~/.semo.env → ~/.claude/semo/.env 이전. 구 경로 폴백 유지.
72
71
  function loadSemoEnv() {
73
- const newEnvFile = path.join(os.homedir(), ".claude", "semo", ".env");
74
- const legacyEnvFile = path.join(os.homedir(), ".semo.env");
75
- const envFile = fs.existsSync(newEnvFile) ? newEnvFile : legacyEnvFile;
72
+ const envFile = path.join(os.homedir(), ".claude", "semo", ".env");
76
73
  if (!fs.existsSync(envFile))
77
74
  return;
78
75
  try {
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * 공용 KEY=VALUE 파서
3
- * ~/.semo.env 파일과 GitHub Gist 콘텐츠 모두 이 함수로 파싱한다.
3
+ * ~/.claude/semo/.env 파일과 GitHub Gist 콘텐츠 모두 이 함수로 파싱한다.
4
4
  */
5
5
  export declare function parseEnvContent(content: string): Record<string, string>;
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseEnvContent = parseEnvContent;
4
4
  /**
5
5
  * 공용 KEY=VALUE 파서
6
- * ~/.semo.env 파일과 GitHub Gist 콘텐츠 모두 이 함수로 파싱한다.
6
+ * ~/.claude/semo/.env 파일과 GitHub Gist 콘텐츠 모두 이 함수로 파싱한다.
7
7
  */
8
8
  function parseEnvContent(content) {
9
9
  const result = {};
package/dist/index.js CHANGED
@@ -331,8 +331,7 @@ async function showToolsStatus() {
331
331
  // === 글로벌 설정 체크 ===
332
332
  function isGlobalSetupDone() {
333
333
  const home = os.homedir();
334
- const hasEnv = fs.existsSync(path.join(home, ".claude", "semo", ".env")) ||
335
- fs.existsSync(path.join(home, ".semo.env")); // 하위 호환
334
+ const hasEnv = fs.existsSync(path.join(home, ".claude", "semo", ".env"));
336
335
  const hasSetup = fs.existsSync(path.join(home, ".claude", "semo", "SOUL.md")) ||
337
336
  fs.existsSync(path.join(home, ".claude", "skills")); // 하위 호환
338
337
  return hasEnv && hasSetup;
@@ -494,9 +493,7 @@ const BASE_MCP_SERVERS = [
494
493
  },
495
494
  ];
496
495
  // === ~/.claude/semo/.env 설정 (자동 감지 → Gist → 프롬프트) ===
497
- // v4.5.0: ~/.semo.env → ~/.claude/semo/.env 이전
498
496
  const SEMO_ENV_PATH = path.join(os.homedir(), ".claude", "semo", ".env");
499
- const LEGACY_ENV_PATH = path.join(os.homedir(), ".semo.env");
500
497
  const SEMO_CREDENTIALS = [
501
498
  {
502
499
  key: "DATABASE_URL",
@@ -544,27 +541,9 @@ function writeSemoEnvFile(creds) {
544
541
  }
545
542
  lines.push("");
546
543
  fs.writeFileSync(SEMO_ENV_PATH, lines.join("\n"), { mode: 0o600 });
547
- // 하위 호환 심링크: ~/.semo.env → ~/.claude/semo/.env
548
- try {
549
- if (fs.existsSync(LEGACY_ENV_PATH)) {
550
- const stat = fs.lstatSync(LEGACY_ENV_PATH);
551
- if (!stat.isSymbolicLink()) {
552
- // 기존 실파일은 백업 후 심링크로 교체
553
- fs.renameSync(LEGACY_ENV_PATH, LEGACY_ENV_PATH + ".bak");
554
- }
555
- else {
556
- fs.unlinkSync(LEGACY_ENV_PATH);
557
- }
558
- }
559
- fs.symlinkSync(SEMO_ENV_PATH, LEGACY_ENV_PATH);
560
- }
561
- catch {
562
- // 심링크 실패 시 무시 — 새 경로가 원본
563
- }
564
544
  }
565
545
  function readSemoEnvCreds() {
566
- // 경로 우선, 없으면 레거시 폴백
567
- const envFile = fs.existsSync(SEMO_ENV_PATH) ? SEMO_ENV_PATH : LEGACY_ENV_PATH;
546
+ const envFile = SEMO_ENV_PATH;
568
547
  if (!fs.existsSync(envFile))
569
548
  return {};
570
549
  try {
@@ -740,7 +719,7 @@ async function buildKbFirstBlock() {
740
719
  const label = s.desc || s.scheme_key;
741
720
  domainGuide += `- 서비스 ${label} → \`domain: {서비스명}\`, key: \`${s.scheme_key}\`\n`;
742
721
  }
743
- domainGuide += `- 서비스 KPI → \`domain: {서비스명}\`, key: \`kpi/current\`\n`;
722
+ domainGuide += `- 서비스 KPI → \`domain: {서비스명}\`, key: \`kpi/{YYYY-MM-DD}\` (최신: 가장 최근 날짜)\n`;
744
723
  domainGuide += `- 서비스 마일스톤 → \`domain: {서비스명}\`, key: \`milestone/{slug}\`\n`;
745
724
  domainGuide += `\n**정확한 경로를 모를 때:**\n`;
746
725
  domainGuide += `1. \`semo kb search "검색어"\` → 결과의 \`[domain] key/sub_key\` 경로 확인\n`;
@@ -853,7 +832,7 @@ async function setupHooks(isUpdate = false) {
853
832
  hooks: [
854
833
  {
855
834
  type: "command",
856
- command: ". ~/.claude/semo/.env 2>/dev/null || . ~/.semo.env 2>/dev/null; semo context sync 2>/dev/null || true",
835
+ command: ". ~/.claude/semo/.env 2>/dev/null; semo context sync 2>/dev/null || true",
857
836
  timeout: 30,
858
837
  },
859
838
  ],
@@ -865,7 +844,7 @@ async function setupHooks(isUpdate = false) {
865
844
  hooks: [
866
845
  {
867
846
  type: "command",
868
- command: ". ~/.claude/semo/.env 2>/dev/null || . ~/.semo.env 2>/dev/null; semo context push 2>/dev/null || true",
847
+ command: ". ~/.claude/semo/.env 2>/dev/null; semo context push 2>/dev/null || true",
869
848
  timeout: 30,
870
849
  },
871
850
  ],
@@ -1119,9 +1098,9 @@ npm run build # 빌드 검증
1119
1098
 
1120
1099
  ---
1121
1100
 
1122
- ## 환경변수 (\`~/.semo.env\`)
1101
+ ## 환경변수 (\`~/.claude/semo/.env\`)
1123
1102
 
1124
- SEMO는 \`~/.semo.env\` 파일에서 팀 공통 환경변수를 로드합니다.
1103
+ SEMO는 \`~/.claude/semo/.env\` 파일에서 팀 공통 환경변수를 로드합니다.
1125
1104
  SessionStart 훅과 OpenClaw 게이트웨이 래퍼에서 자동 source됩니다.
1126
1105
 
1127
1106
  | 변수 | 용도 | 필수 |
@@ -1130,7 +1109,7 @@ SessionStart 훅과 OpenClaw 게이트웨이 래퍼에서 자동 source됩니다
1130
1109
  | \`OPENAI_API_KEY\` | KB 임베딩용 (text-embedding-3-small) | 선택 |
1131
1110
  | \`SLACK_WEBHOOK\` | Slack 알림 | 선택 |
1132
1111
 
1133
- 키 갱신이 필요하면 \`~/.semo.env\`를 직접 편집하거나 \`semo onboarding -f\`를 실행하세요.
1112
+ 키 갱신이 필요하면 \`~/.claude/semo/.env\`를 직접 편집하거나 \`semo onboarding -f\`를 실행하세요.
1134
1113
 
1135
1114
  ---
1136
1115
 
@@ -1754,7 +1733,7 @@ kbCmd
1754
1733
  kbCmd
1755
1734
  .command("ontology")
1756
1735
  .description("온톨로지 조회 — 도메인/타입/스키마/라우팅 테이블")
1757
- .option("--action <type>", "동작 (list|show|services|types|instances|schema|routing-table|register|add-key|remove-key)", "list")
1736
+ .option("--action <type>", "동작 (list|show|services|types|instances|schema|routing-table|register|unregister|create-type|add-key|remove-key)", "list")
1758
1737
  .option("--domain <name>", "action=show|register 시 도메인")
1759
1738
  .option("--type <name>", "action=schema|register|add-key|remove-key 시 타입 키")
1760
1739
  .option("--key <name>", "action=add-key|remove-key 시 스키마 키")
@@ -1765,6 +1744,8 @@ kbCmd
1765
1744
  .option("--service <name>", "action=register 시 서비스 그룹")
1766
1745
  .option("--tags <tags>", "action=register 시 태그 (쉼표 구분)")
1767
1746
  .option("--no-init", "action=register 시 필수 KB entry 자동 생성 건너뛰기")
1747
+ .option("--force", "action=unregister 시 잔존 KB 항목도 모두 삭제")
1748
+ .option("--yes", "action=unregister 시 확인 프롬프트 건너뛰기")
1768
1749
  .option("--format <type>", "출력 형식 (json|table)", "table")
1769
1750
  .action(async (options) => {
1770
1751
  try {
@@ -1949,6 +1930,24 @@ kbCmd
1949
1930
  }
1950
1931
  }
1951
1932
  }
1933
+ else if (action === "create-type") {
1934
+ if (!options.type) {
1935
+ console.log(chalk_1.default.red("--type 옵션이 필요합니다. (예: --type project)"));
1936
+ process.exit(1);
1937
+ }
1938
+ const result = await (0, kb_1.ontoCreateType)(pool, {
1939
+ type_key: options.type,
1940
+ description: options.description,
1941
+ });
1942
+ if (result.success) {
1943
+ console.log(chalk_1.default.green(`\n✅ 온톨로지 타입 '${options.type}' 생성 완료`));
1944
+ console.log(chalk_1.default.gray(`\n 다음 단계: semo kb ontology --action add-key --type ${options.type} --key <key> --key-type singleton|collection\n`));
1945
+ }
1946
+ else {
1947
+ console.log(chalk_1.default.red(`\n❌ 타입 생성 실패: ${result.error}\n`));
1948
+ process.exit(1);
1949
+ }
1950
+ }
1952
1951
  else if (action === "add-key") {
1953
1952
  if (!options.type) {
1954
1953
  console.log(chalk_1.default.red("--type 옵션이 필요합니다. (예: --type service)"));
@@ -1992,8 +1991,64 @@ kbCmd
1992
1991
  process.exit(1);
1993
1992
  }
1994
1993
  }
1994
+ else if (action === "unregister") {
1995
+ if (!options.domain) {
1996
+ console.log(chalk_1.default.red("--domain 옵션이 필요합니다."));
1997
+ process.exit(1);
1998
+ }
1999
+ // 도메인 정보 조회
2000
+ const domainInfo = await (0, kb_1.ontoShow)(pool, options.domain);
2001
+ if (!domainInfo) {
2002
+ console.log(chalk_1.default.red(`\n❌ 도메인 '${options.domain}'은(는) 존재하지 않습니다.\n`));
2003
+ process.exit(1);
2004
+ }
2005
+ // KB 엔트리 수 확인
2006
+ const countRes = await pool.query("SELECT COUNT(*)::int AS cnt FROM semo.knowledge_base WHERE domain = $1", [options.domain]);
2007
+ const kbCount = countRes.rows[0].cnt;
2008
+ // 확인 프롬프트
2009
+ if (!options.yes) {
2010
+ const typeStr = domainInfo.entity_type ? ` [${domainInfo.entity_type}]` : "";
2011
+ const svcStr = domainInfo.service && domainInfo.service !== "_global" ? ` (${domainInfo.service})` : "";
2012
+ if (kbCount > 0 && options.force) {
2013
+ console.log(chalk_1.default.yellow(`\n⚠️ 도메인 '${options.domain}'에 KB 항목 ${kbCount}건이 남아있습니다.`));
2014
+ console.log(chalk_1.default.yellow(` --force 옵션으로 모두 삭제됩니다.\n`));
2015
+ }
2016
+ else {
2017
+ console.log(chalk_1.default.cyan(`\n📐 삭제 대상 도메인: ${options.domain}${typeStr}${svcStr}`));
2018
+ console.log(chalk_1.default.gray(` KB 항목: ${kbCount}건\n`));
2019
+ }
2020
+ const { createInterface } = await import("readline");
2021
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2022
+ const answer = await new Promise((resolve) => {
2023
+ rl.question(chalk_1.default.yellow(" 정말 삭제하시겠습니까? (y/N): "), resolve);
2024
+ });
2025
+ rl.close();
2026
+ if (answer.toLowerCase() !== "y") {
2027
+ console.log(chalk_1.default.gray(" 취소됨.\n"));
2028
+ await (0, database_1.closeConnection)();
2029
+ return;
2030
+ }
2031
+ }
2032
+ const result = await (0, kb_1.ontoUnregister)(pool, options.domain, !!options.force);
2033
+ if (options.format === "json") {
2034
+ console.log(JSON.stringify(result, null, 2));
2035
+ }
2036
+ else {
2037
+ if (result.success) {
2038
+ console.log(chalk_1.default.green(`\n✅ 도메인 '${options.domain}' 삭제 완료`));
2039
+ if (result.deleted_entries && result.deleted_entries > 0) {
2040
+ console.log(chalk_1.default.gray(` KB 항목 ${result.deleted_entries}건 함께 삭제됨`));
2041
+ }
2042
+ console.log();
2043
+ }
2044
+ else {
2045
+ console.log(chalk_1.default.red(`\n❌ 삭제 실패: ${result.error}\n`));
2046
+ process.exit(1);
2047
+ }
2048
+ }
2049
+ }
1995
2050
  else {
1996
- console.log(chalk_1.default.red(`알 수 없는 action: '${action}'. 사용 가능: list, show, services, types, instances, schema, routing-table, register, add-key, remove-key`));
2051
+ console.log(chalk_1.default.red(`알 수 없는 action: '${action}'. 사용 가능: list, show, services, types, instances, schema, routing-table, register, create-type, add-key, remove-key, unregister`));
1997
2052
  process.exit(1);
1998
2053
  }
1999
2054
  await (0, database_1.closeConnection)();
@@ -2221,6 +2276,74 @@ ontoCmd
2221
2276
  process.exit(1);
2222
2277
  }
2223
2278
  });
2279
+ ontoCmd
2280
+ .command("unregister <domain>")
2281
+ .description("온톨로지 도메인 삭제 (KB 데이터 포함)")
2282
+ .option("--force", "잔존 KB 항목이 있어도 모두 삭제 후 도메인 제거")
2283
+ .option("--yes", "확인 프롬프트 건너뛰기")
2284
+ .option("--format <type>", "출력 형식 (json|table)", "table")
2285
+ .action(async (domain, options) => {
2286
+ try {
2287
+ const pool = (0, database_1.getPool)();
2288
+ // 도메인 정보 조회
2289
+ const domainInfo = await (0, kb_1.ontoShow)(pool, domain);
2290
+ if (!domainInfo) {
2291
+ console.log(chalk_1.default.red(`\n❌ 도메인 '${domain}'은(는) 존재하지 않습니다.\n`));
2292
+ await (0, database_1.closeConnection)();
2293
+ process.exit(1);
2294
+ }
2295
+ // KB 엔트리 수 확인
2296
+ const countRes = await pool.query("SELECT COUNT(*)::int AS cnt FROM semo.knowledge_base WHERE domain = $1", [domain]);
2297
+ const kbCount = countRes.rows[0].cnt;
2298
+ // 확인 프롬프트
2299
+ if (!options.yes) {
2300
+ const typeStr = domainInfo.entity_type ? ` [${domainInfo.entity_type}]` : "";
2301
+ const svcStr = domainInfo.service && domainInfo.service !== "_global" ? ` (${domainInfo.service})` : "";
2302
+ if (kbCount > 0 && options.force) {
2303
+ console.log(chalk_1.default.yellow(`\n⚠️ 도메인 '${domain}'에 KB 항목 ${kbCount}건이 남아있습니다.`));
2304
+ console.log(chalk_1.default.yellow(` --force 옵션으로 모두 삭제됩니다.\n`));
2305
+ }
2306
+ else {
2307
+ console.log(chalk_1.default.cyan(`\n📐 삭제 대상 도메인: ${domain}${typeStr}${svcStr}`));
2308
+ console.log(chalk_1.default.gray(` KB 항목: ${kbCount}건\n`));
2309
+ }
2310
+ const { createInterface } = await import("readline");
2311
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2312
+ const answer = await new Promise((resolve) => {
2313
+ rl.question(chalk_1.default.yellow(" 정말 삭제하시겠습니까? (y/N): "), resolve);
2314
+ });
2315
+ rl.close();
2316
+ if (answer.toLowerCase() !== "y") {
2317
+ console.log(chalk_1.default.gray(" 취소됨.\n"));
2318
+ await (0, database_1.closeConnection)();
2319
+ return;
2320
+ }
2321
+ }
2322
+ const result = await (0, kb_1.ontoUnregister)(pool, domain, !!options.force);
2323
+ if (options.format === "json") {
2324
+ console.log(JSON.stringify(result, null, 2));
2325
+ }
2326
+ else {
2327
+ if (result.success) {
2328
+ console.log(chalk_1.default.green(`\n✅ 도메인 '${domain}' 삭제 완료`));
2329
+ if (result.deleted_entries && result.deleted_entries > 0) {
2330
+ console.log(chalk_1.default.gray(` KB 항목 ${result.deleted_entries}건 함께 삭제됨`));
2331
+ }
2332
+ console.log();
2333
+ }
2334
+ else {
2335
+ console.log(chalk_1.default.red(`\n❌ 삭제 실패: ${result.error}\n`));
2336
+ process.exit(1);
2337
+ }
2338
+ }
2339
+ await (0, database_1.closeConnection)();
2340
+ }
2341
+ catch (err) {
2342
+ console.error(chalk_1.default.red(`삭제 실패: ${err}`));
2343
+ await (0, database_1.closeConnection)();
2344
+ process.exit(1);
2345
+ }
2346
+ });
2224
2347
  // === 신규 v4 커맨드 그룹 등록 ===
2225
2348
  (0, context_1.registerContextCommands)(program);
2226
2349
  (0, bots_1.registerBotsCommands)(program);
package/dist/kb.d.ts CHANGED
@@ -232,6 +232,17 @@ export interface OntoRegisterResult {
232
232
  * 4. If init_required (default true), create KB entries for required keys in kb_type_schema
233
233
  */
234
234
  export declare function ontoRegister(pool: Pool, opts: OntoRegisterOptions): Promise<OntoRegisterResult>;
235
+ export interface OntoUnregisterResult {
236
+ success: boolean;
237
+ deleted_entries?: number;
238
+ error?: string;
239
+ }
240
+ /**
241
+ * Unregister an ontology domain.
242
+ * If force=false and KB entries exist, returns error with count.
243
+ * If force=true, deletes all KB entries in a transaction, then removes the domain.
244
+ */
245
+ export declare function ontoUnregister(pool: Pool, domain: string, force: boolean): Promise<OntoUnregisterResult>;
235
246
  /**
236
247
  * Add a key to a type schema
237
248
  */
@@ -246,6 +257,17 @@ export declare function ontoAddKey(pool: Pool, opts: {
246
257
  success: boolean;
247
258
  error?: string;
248
259
  }>;
260
+ /**
261
+ * Create a new ontology type
262
+ */
263
+ export declare function ontoCreateType(pool: Pool, opts: {
264
+ type_key: string;
265
+ description?: string;
266
+ schema?: Record<string, unknown>;
267
+ }): Promise<{
268
+ success: boolean;
269
+ error?: string;
270
+ }>;
249
271
  /**
250
272
  * Remove a key from a type schema
251
273
  */
package/dist/kb.js CHANGED
@@ -62,7 +62,9 @@ exports.ontoRoutingTable = ontoRoutingTable;
62
62
  exports.ontoListServices = ontoListServices;
63
63
  exports.ontoListInstances = ontoListInstances;
64
64
  exports.ontoRegister = ontoRegister;
65
+ exports.ontoUnregister = ontoUnregister;
65
66
  exports.ontoAddKey = ontoAddKey;
67
+ exports.ontoCreateType = ontoCreateType;
66
68
  exports.ontoRemoveKey = ontoRemoveKey;
67
69
  exports.ontoPullToLocal = ontoPullToLocal;
68
70
  const fs = __importStar(require("fs"));
@@ -1010,6 +1012,50 @@ async function ontoRegister(pool, opts) {
1010
1012
  client.release();
1011
1013
  }
1012
1014
  }
1015
+ /**
1016
+ * Unregister an ontology domain.
1017
+ * If force=false and KB entries exist, returns error with count.
1018
+ * If force=true, deletes all KB entries in a transaction, then removes the domain.
1019
+ */
1020
+ async function ontoUnregister(pool, domain, force) {
1021
+ const client = await pool.connect();
1022
+ try {
1023
+ // 1. Check domain exists
1024
+ const domainCheck = await client.query("SELECT domain, entity_type, service FROM semo.ontology WHERE domain = $1", [domain]);
1025
+ if (domainCheck.rows.length === 0) {
1026
+ return { success: false, error: `도메인 '${domain}'은(는) 존재하지 않습니다.` };
1027
+ }
1028
+ // 2. Count KB entries
1029
+ const countResult = await client.query("SELECT COUNT(*)::int AS cnt FROM semo.knowledge_base WHERE domain = $1", [domain]);
1030
+ const entryCount = countResult.rows[0].cnt;
1031
+ // 3. If entries exist and no force → error
1032
+ if (entryCount > 0 && !force) {
1033
+ return {
1034
+ success: false,
1035
+ error: `도메인 '${domain}'에 KB 항목 ${entryCount}건이 남아있습니다. --force 옵션으로 모두 삭제 후 제거할 수 있습니다.`,
1036
+ };
1037
+ }
1038
+ // 4. Transaction: delete KB entries (if any) → delete ontology
1039
+ await client.query("BEGIN");
1040
+ try {
1041
+ let deletedEntries = 0;
1042
+ if (entryCount > 0) {
1043
+ const delResult = await client.query("DELETE FROM semo.knowledge_base WHERE domain = $1", [domain]);
1044
+ deletedEntries = delResult.rowCount ?? 0;
1045
+ }
1046
+ await client.query("DELETE FROM semo.ontology WHERE domain = $1", [domain]);
1047
+ await client.query("COMMIT");
1048
+ return { success: true, deleted_entries: deletedEntries };
1049
+ }
1050
+ catch (err) {
1051
+ await client.query("ROLLBACK");
1052
+ return { success: false, error: String(err) };
1053
+ }
1054
+ }
1055
+ finally {
1056
+ client.release();
1057
+ }
1058
+ }
1013
1059
  /**
1014
1060
  * Add a key to a type schema
1015
1061
  */
@@ -1057,6 +1103,36 @@ async function ontoAddKey(pool, opts) {
1057
1103
  client.release();
1058
1104
  }
1059
1105
  }
1106
+ /**
1107
+ * Create a new ontology type
1108
+ */
1109
+ async function ontoCreateType(pool, opts) {
1110
+ // kebab-case 검증
1111
+ if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(opts.type_key)) {
1112
+ return { success: false, error: `타입 키 '${opts.type_key}'이(가) 유효하지 않습니다. kebab-case 소문자만 사용 가능 (예: my-type)` };
1113
+ }
1114
+ const client = await pool.connect();
1115
+ try {
1116
+ // 중복 확인
1117
+ const dupCheck = await client.query("SELECT type_key FROM semo.ontology_types WHERE type_key = $1", [opts.type_key]);
1118
+ if (dupCheck.rows.length > 0) {
1119
+ return { success: false, error: `타입 '${opts.type_key}'은(는) 이미 존재합니다.` };
1120
+ }
1121
+ await client.query(`INSERT INTO semo.ontology_types (type_key, schema, description)
1122
+ VALUES ($1, $2, $3)`, [
1123
+ opts.type_key,
1124
+ JSON.stringify(opts.schema || {}),
1125
+ opts.description || opts.type_key,
1126
+ ]);
1127
+ return { success: true };
1128
+ }
1129
+ catch (err) {
1130
+ return { success: false, error: String(err) };
1131
+ }
1132
+ finally {
1133
+ client.release();
1134
+ }
1135
+ }
1060
1136
  /**
1061
1137
  * Remove a key from a type schema
1062
1138
  */
@@ -2,7 +2,7 @@
2
2
  * Slack 알림 유틸리티
3
3
  *
4
4
  * SLACK_WEBHOOK 환경변수에서 URL을 읽어 알림 전송.
5
- * ~/.semo.env에서 자동 로드됨 (database.ts loadSemoEnv).
5
+ * ~/.claude/semo/.env에서 자동 로드됨 (database.ts loadSemoEnv).
6
6
  */
7
7
  export declare function sendSlackNotification(message: string, webhookUrl?: string): Promise<boolean>;
8
8
  export declare function formatTestFailureMessage(suiteId: string, runId: string, pass: number, fail: number, warn: number, failedLabels: string[]): string;
@@ -3,7 +3,7 @@
3
3
  * Slack 알림 유틸리티
4
4
  *
5
5
  * SLACK_WEBHOOK 환경변수에서 URL을 읽어 알림 전송.
6
- * ~/.semo.env에서 자동 로드됨 (database.ts loadSemoEnv).
6
+ * ~/.claude/semo/.env에서 자동 로드됨 (database.ts loadSemoEnv).
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.sendSlackNotification = sendSlackNotification;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semo-cli",
3
- "version": "4.9.0",
3
+ "version": "4.11.0",
4
4
  "description": "SEMO CLI - AI Agent Orchestration Framework Installer",
5
5
  "main": "dist/index.js",
6
6
  "bin": {