@team-semicolon/semo-cli 4.10.0 → 4.12.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.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * semo commitments — 봇 약속 추적 (Durable Task Tracking)
3
+ *
4
+ * 봇이 "~하겠습니다" 약속 후 세션 종료되어도 추적 가능하도록
5
+ * semo.bot_commitments 테이블에 기록하고, 워치독이 overdue/stale 감지.
6
+ */
7
+ import { Command } from "commander";
8
+ export declare function registerCommitmentsCommands(program: Command): void;
@@ -0,0 +1,361 @@
1
+ "use strict";
2
+ /**
3
+ * semo commitments — 봇 약속 추적 (Durable Task Tracking)
4
+ *
5
+ * 봇이 "~하겠습니다" 약속 후 세션 종료되어도 추적 가능하도록
6
+ * semo.bot_commitments 테이블에 기록하고, 워치독이 overdue/stale 감지.
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.registerCommitmentsCommands = registerCommitmentsCommands;
13
+ const chalk_1 = __importDefault(require("chalk"));
14
+ const database_1 = require("../database");
15
+ // ─── ID 생성 ─────────────────────────────────────────────────────────────────
16
+ function generateCommitmentId(botId) {
17
+ const rand = Math.random().toString(36).slice(2, 6);
18
+ return `cmt-${botId}-${Date.now()}-${rand}`;
19
+ }
20
+ // ─── deadline 파싱 ───────────────────────────────────────────────────────────
21
+ function parseDeadline(input) {
22
+ const match = input.match(/^(\d+)(m|h|d)$/);
23
+ if (match) {
24
+ const value = parseInt(match[1]);
25
+ const unit = match[2];
26
+ const now = new Date();
27
+ if (unit === "m")
28
+ now.setMinutes(now.getMinutes() + value);
29
+ else if (unit === "h")
30
+ now.setHours(now.getHours() + value);
31
+ else if (unit === "d")
32
+ now.setDate(now.getDate() + value);
33
+ return now.toISOString();
34
+ }
35
+ // ISO 문자열이면 그대로
36
+ return new Date(input).toISOString();
37
+ }
38
+ // ─── Command registration ───────────────────────────────────────────────────
39
+ function registerCommitmentsCommands(program) {
40
+ const cmd = program
41
+ .command("commitments")
42
+ .description("봇 약속 추적 (Durable Commitment Tracking)");
43
+ // ── semo commitments create ────────────────────────────────────────────────
44
+ cmd
45
+ .command("create")
46
+ .description("새 약속 등록")
47
+ .requiredOption("--bot-id <id>", "봇 ID")
48
+ .requiredOption("--title <text>", "약속 제목")
49
+ .option("--description <text>", "상세 설명")
50
+ .option("--source-type <type>", "출처 (github-issue|slack|cron|manual)")
51
+ .option("--source-ref <ref>", "출처 참조 (issue URL, channel:thread)")
52
+ .option("--deadline <time>", "기한 (15m, 1h, 2d, 또는 ISO 문자열)")
53
+ .option("--steps <json>", "단계 JSON 배열 (예: \'[{\"label\":\"Step1\",\"done\":false}]\')")
54
+ .action(async (options) => {
55
+ const connected = await (0, database_1.isDbConnected)();
56
+ if (!connected) {
57
+ console.error(chalk_1.default.red("❌ DB 연결 실패"));
58
+ await (0, database_1.closeConnection)();
59
+ process.exit(1);
60
+ }
61
+ const id = generateCommitmentId(options.botId);
62
+ const deadlineAt = options.deadline ? parseDeadline(options.deadline) : null;
63
+ let steps = [];
64
+ if (options.steps) {
65
+ try {
66
+ steps = JSON.parse(options.steps);
67
+ }
68
+ catch {
69
+ console.error(chalk_1.default.red("❌ --steps JSON 파싱 실패"));
70
+ await (0, database_1.closeConnection)();
71
+ process.exit(1);
72
+ }
73
+ }
74
+ try {
75
+ const pool = (0, database_1.getPool)();
76
+ await pool.query(`INSERT INTO semo.bot_commitments
77
+ (id, bot_id, status, title, description, source_type, source_ref, deadline_at, steps)
78
+ VALUES ($1, $2, 'pending', $3, $4, $5, $6, $7, $8)`, [
79
+ id,
80
+ options.botId,
81
+ options.title,
82
+ options.description ?? null,
83
+ options.sourceType ?? null,
84
+ options.sourceRef ?? null,
85
+ deadlineAt,
86
+ JSON.stringify(steps),
87
+ ]);
88
+ console.log(chalk_1.default.green(`✔ commitment created: ${id}`));
89
+ console.log(JSON.stringify({ id, bot_id: options.botId, title: options.title, deadline_at: deadlineAt }));
90
+ }
91
+ catch (err) {
92
+ console.error(chalk_1.default.red(`❌ create 실패: ${err}`));
93
+ process.exit(1);
94
+ }
95
+ finally {
96
+ await (0, database_1.closeConnection)();
97
+ }
98
+ });
99
+ // ── semo commitments update ────────────────────────────────────────────────
100
+ cmd
101
+ .command("update <id>")
102
+ .description("진행 보고 (heartbeat, status, step 완료)")
103
+ .option("--heartbeat", "heartbeat 갱신")
104
+ .option("--status <status>", "상태 변경 (active|pending)")
105
+ .option("--step-done <label>", "특정 step을 완료 처리")
106
+ .action(async (id, options) => {
107
+ const connected = await (0, database_1.isDbConnected)();
108
+ if (!connected) {
109
+ console.error(chalk_1.default.red("❌ DB 연결 실패"));
110
+ await (0, database_1.closeConnection)();
111
+ process.exit(1);
112
+ }
113
+ try {
114
+ const pool = (0, database_1.getPool)();
115
+ if (options.heartbeat) {
116
+ await pool.query(`UPDATE semo.bot_commitments
117
+ SET last_heartbeat_at = NOW(),
118
+ status = CASE WHEN status = 'pending' THEN 'active' ELSE status END
119
+ WHERE id = $1`, [id]);
120
+ console.log(chalk_1.default.green(`✔ heartbeat updated: ${id}`));
121
+ }
122
+ if (options.status) {
123
+ await pool.query(`UPDATE semo.bot_commitments SET status = $1 WHERE id = $2`, [options.status, id]);
124
+ console.log(chalk_1.default.green(`✔ status → ${options.status}: ${id}`));
125
+ }
126
+ if (options.stepDone) {
127
+ await pool.query(`UPDATE semo.bot_commitments
128
+ SET steps = (
129
+ SELECT jsonb_agg(
130
+ CASE
131
+ WHEN elem->>'label' = $2 THEN jsonb_set(elem, '{done}', 'true')
132
+ ELSE elem
133
+ END
134
+ )
135
+ FROM jsonb_array_elements(steps) AS elem
136
+ )
137
+ WHERE id = $1`, [id, options.stepDone]);
138
+ console.log(chalk_1.default.green(`✔ step done "${options.stepDone}": ${id}`));
139
+ }
140
+ }
141
+ catch (err) {
142
+ console.error(chalk_1.default.red(`❌ update 실패: ${err}`));
143
+ process.exit(1);
144
+ }
145
+ finally {
146
+ await (0, database_1.closeConnection)();
147
+ }
148
+ });
149
+ // ── semo commitments done ──────────────────────────────────────────────────
150
+ cmd
151
+ .command("done <id>")
152
+ .description("약속 완료 처리")
153
+ .action(async (id) => {
154
+ const connected = await (0, database_1.isDbConnected)();
155
+ if (!connected) {
156
+ console.error(chalk_1.default.red("❌ DB 연결 실패"));
157
+ await (0, database_1.closeConnection)();
158
+ process.exit(1);
159
+ }
160
+ try {
161
+ const pool = (0, database_1.getPool)();
162
+ const result = await pool.query(`UPDATE semo.bot_commitments SET status = 'done' WHERE id = $1 AND status IN ('pending', 'active') RETURNING id`, [id]);
163
+ if (result.rowCount === 0) {
164
+ console.error(chalk_1.default.yellow(`⚠ commitment 없거나 이미 종료됨: ${id}`));
165
+ }
166
+ else {
167
+ console.log(chalk_1.default.green(`✔ commitment done: ${id}`));
168
+ }
169
+ }
170
+ catch (err) {
171
+ console.error(chalk_1.default.red(`❌ done 실패: ${err}`));
172
+ process.exit(1);
173
+ }
174
+ finally {
175
+ await (0, database_1.closeConnection)();
176
+ }
177
+ });
178
+ // ── semo commitments fail ──────────────────────────────────────────────────
179
+ cmd
180
+ .command("fail <id>")
181
+ .description("약속 실패 처리")
182
+ .option("--reason <text>", "실패 사유")
183
+ .action(async (id, options) => {
184
+ const connected = await (0, database_1.isDbConnected)();
185
+ if (!connected) {
186
+ console.error(chalk_1.default.red("❌ DB 연결 실패"));
187
+ await (0, database_1.closeConnection)();
188
+ process.exit(1);
189
+ }
190
+ try {
191
+ const pool = (0, database_1.getPool)();
192
+ const metadataUpdate = options.reason
193
+ ? `, metadata = metadata || jsonb_build_object('fail_reason', $2::text)`
194
+ : "";
195
+ const params = options.reason ? [id, options.reason] : [id];
196
+ const result = await pool.query(`UPDATE semo.bot_commitments SET status = 'failed'${metadataUpdate} WHERE id = $1 AND status IN ('pending', 'active') RETURNING id`, params);
197
+ if (result.rowCount === 0) {
198
+ console.error(chalk_1.default.yellow(`⚠ commitment 없거나 이미 종료됨: ${id}`));
199
+ }
200
+ else {
201
+ console.log(chalk_1.default.green(`✔ commitment failed: ${id}`));
202
+ }
203
+ }
204
+ catch (err) {
205
+ console.error(chalk_1.default.red(`❌ fail 실패: ${err}`));
206
+ process.exit(1);
207
+ }
208
+ finally {
209
+ await (0, database_1.closeConnection)();
210
+ }
211
+ });
212
+ // ── semo commitments list ──────────────────────────────────────────────────
213
+ cmd
214
+ .command("list")
215
+ .description("약속 목록 조회")
216
+ .option("--bot-id <id>", "특정 봇만")
217
+ .option("--status <status>", "상태 필터 (pending|active|done|failed|expired)")
218
+ .option("--limit <n>", "최대 조회 수", "20")
219
+ .option("--format <type>", "출력 형식 (table|json)", "table")
220
+ .action(async (options) => {
221
+ const connected = await (0, database_1.isDbConnected)();
222
+ if (!connected) {
223
+ console.error(chalk_1.default.red("❌ DB 연결 실패"));
224
+ await (0, database_1.closeConnection)();
225
+ process.exit(1);
226
+ }
227
+ try {
228
+ const pool = (0, database_1.getPool)();
229
+ const conditions = [];
230
+ const params = [];
231
+ let paramIdx = 1;
232
+ if (options.botId) {
233
+ conditions.push(`bot_id = $${paramIdx++}`);
234
+ params.push(options.botId);
235
+ }
236
+ if (options.status) {
237
+ conditions.push(`status = $${paramIdx++}`);
238
+ params.push(options.status);
239
+ }
240
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
241
+ params.push(parseInt(options.limit));
242
+ const result = await pool.query(`SELECT id, bot_id, status, title, deadline_at::text, steps,
243
+ last_heartbeat_at::text, created_at::text, completed_at::text, metadata
244
+ FROM semo.bot_commitments
245
+ ${where}
246
+ ORDER BY created_at DESC
247
+ LIMIT $${paramIdx}`, params);
248
+ if (options.format === "json") {
249
+ console.log(JSON.stringify(result.rows, null, 2));
250
+ }
251
+ else {
252
+ console.log(chalk_1.default.cyan.bold("\n📋 Commitments\n"));
253
+ if (result.rows.length === 0) {
254
+ console.log(chalk_1.default.yellow(" 약속 없음"));
255
+ }
256
+ else {
257
+ for (const c of result.rows) {
258
+ const statusColor = c.status === "done" ? chalk_1.default.green :
259
+ c.status === "failed" ? chalk_1.default.red :
260
+ c.status === "expired" ? chalk_1.default.gray :
261
+ c.status === "active" ? chalk_1.default.cyan :
262
+ chalk_1.default.yellow;
263
+ const deadline = c.deadline_at
264
+ ? new Date(c.deadline_at).toLocaleString("ko-KR")
265
+ : "-";
266
+ const stepsInfo = Array.isArray(c.steps) && c.steps.length > 0
267
+ ? ` [${c.steps.filter((s) => s.done).length}/${c.steps.length}]`
268
+ : "";
269
+ console.log(` ${statusColor(c.status.padEnd(8))} ` +
270
+ chalk_1.default.white(c.title.slice(0, 40).padEnd(42)) +
271
+ chalk_1.default.gray(`${c.bot_id.padEnd(14)}`) +
272
+ chalk_1.default.gray(`⏰ ${deadline}`) +
273
+ chalk_1.default.cyan(stepsInfo));
274
+ console.log(chalk_1.default.gray(` ${c.id}`));
275
+ }
276
+ }
277
+ console.log();
278
+ }
279
+ }
280
+ catch (err) {
281
+ console.error(chalk_1.default.red(`❌ list 실패: ${err}`));
282
+ process.exit(1);
283
+ }
284
+ finally {
285
+ await (0, database_1.closeConnection)();
286
+ }
287
+ });
288
+ // ── semo commitments watch ─────────────────────────────────────────────────
289
+ cmd
290
+ .command("watch")
291
+ .description("워치독 — 활성 약속의 health 상태 조회")
292
+ .option("--format <type>", "출력 형식 (table|json)", "table")
293
+ .action(async (options) => {
294
+ const connected = await (0, database_1.isDbConnected)();
295
+ if (!connected) {
296
+ console.error(chalk_1.default.red("❌ DB 연결 실패"));
297
+ await (0, database_1.closeConnection)();
298
+ process.exit(1);
299
+ }
300
+ try {
301
+ const pool = (0, database_1.getPool)();
302
+ const result = await pool.query(`SELECT id, bot_id, status, title, deadline_at::text,
303
+ health, minutes_since_heartbeat, minutes_overdue,
304
+ steps, last_heartbeat_at::text, created_at::text, metadata
305
+ FROM semo.v_active_commitments
306
+ ORDER BY
307
+ CASE health
308
+ WHEN 'overdue' THEN 0
309
+ WHEN 'stale' THEN 1
310
+ ELSE 2
311
+ END,
312
+ deadline_at ASC NULLS LAST`);
313
+ if (options.format === "json") {
314
+ const summary = {
315
+ total: result.rows.length,
316
+ overdue: result.rows.filter((r) => r.health === "overdue").length,
317
+ stale: result.rows.filter((r) => r.health === "stale").length,
318
+ on_track: result.rows.filter((r) => r.health === "on-track").length,
319
+ commitments: result.rows,
320
+ };
321
+ console.log(JSON.stringify(summary, null, 2));
322
+ }
323
+ else {
324
+ console.log(chalk_1.default.cyan.bold("\n🔍 Commitment Watchdog\n"));
325
+ if (result.rows.length === 0) {
326
+ console.log(chalk_1.default.green(" ✅ 활성 약속 없음 — all clear"));
327
+ }
328
+ else {
329
+ for (const c of result.rows) {
330
+ const healthColor = c.health === "overdue" ? chalk_1.default.red.bold :
331
+ c.health === "stale" ? chalk_1.default.yellow :
332
+ chalk_1.default.green;
333
+ const overdue = c.minutes_overdue != null && c.minutes_overdue > 0
334
+ ? ` (${Math.round(c.minutes_overdue)}분 초과)`
335
+ : "";
336
+ console.log(` ${healthColor(c.health.padEnd(10))} ` +
337
+ chalk_1.default.white(c.title.slice(0, 35).padEnd(37)) +
338
+ chalk_1.default.gray(c.bot_id.padEnd(14)) +
339
+ chalk_1.default.gray(`hb: ${Math.round(c.minutes_since_heartbeat)}분 전`) +
340
+ chalk_1.default.red(overdue));
341
+ }
342
+ const overdue = result.rows.filter((r) => r.health === "overdue").length;
343
+ const stale = result.rows.filter((r) => r.health === "stale").length;
344
+ console.log();
345
+ if (overdue > 0)
346
+ console.log(chalk_1.default.red.bold(` ⚠ ${overdue}건 overdue`));
347
+ if (stale > 0)
348
+ console.log(chalk_1.default.yellow(` ⏳ ${stale}건 stale`));
349
+ }
350
+ console.log();
351
+ }
352
+ }
353
+ catch (err) {
354
+ console.error(chalk_1.default.red(`❌ watch 실패: ${err}`));
355
+ process.exit(1);
356
+ }
357
+ finally {
358
+ await (0, database_1.closeConnection)();
359
+ }
360
+ });
361
+ }
@@ -75,6 +75,30 @@ export interface BotProtocol {
75
75
  value: Record<string, unknown>;
76
76
  description: string | null;
77
77
  }
78
+ export interface BotCommitment {
79
+ id: string;
80
+ bot_id: string;
81
+ status: 'pending' | 'active' | 'done' | 'failed' | 'expired';
82
+ title: string;
83
+ description: string | null;
84
+ source_type: string | null;
85
+ source_ref: string | null;
86
+ deadline_at: string | null;
87
+ steps: Array<{
88
+ label: string;
89
+ done: boolean;
90
+ }>;
91
+ metadata: Record<string, unknown>;
92
+ last_heartbeat_at: string | null;
93
+ created_at: string;
94
+ updated_at: string;
95
+ completed_at: string | null;
96
+ }
97
+ export interface ActiveCommitmentView extends BotCommitment {
98
+ health: 'overdue' | 'stale' | 'on-track';
99
+ minutes_since_heartbeat: number;
100
+ minutes_overdue: number | null;
101
+ }
78
102
  /**
79
103
  * 활성 스킬 목록 조회
80
104
  * SoT: skill_definitions (prompt as content — CLI 인터페이스 유지)
package/dist/index.js CHANGED
@@ -65,6 +65,7 @@ const sessions_1 = require("./commands/sessions");
65
65
  const db_1 = require("./commands/db");
66
66
  const memory_1 = require("./commands/memory");
67
67
  const test_1 = require("./commands/test");
68
+ const commitments_1 = require("./commands/commitments");
68
69
  const global_cache_1 = require("./global-cache");
69
70
  const semo_workspace_1 = require("./semo-workspace");
70
71
  const PACKAGE_NAME = "@team-semicolon/semo-cli";
@@ -1733,7 +1734,7 @@ kbCmd
1733
1734
  kbCmd
1734
1735
  .command("ontology")
1735
1736
  .description("온톨로지 조회 — 도메인/타입/스키마/라우팅 테이블")
1736
- .option("--action <type>", "동작 (list|show|services|types|instances|schema|routing-table|register|create-type|add-key|remove-key)", "list")
1737
+ .option("--action <type>", "동작 (list|show|services|types|instances|schema|routing-table|register|unregister|create-type|add-key|remove-key)", "list")
1737
1738
  .option("--domain <name>", "action=show|register 시 도메인")
1738
1739
  .option("--type <name>", "action=schema|register|add-key|remove-key 시 타입 키")
1739
1740
  .option("--key <name>", "action=add-key|remove-key 시 스키마 키")
@@ -1744,6 +1745,8 @@ kbCmd
1744
1745
  .option("--service <name>", "action=register 시 서비스 그룹")
1745
1746
  .option("--tags <tags>", "action=register 시 태그 (쉼표 구분)")
1746
1747
  .option("--no-init", "action=register 시 필수 KB entry 자동 생성 건너뛰기")
1748
+ .option("--force", "action=unregister 시 잔존 KB 항목도 모두 삭제")
1749
+ .option("--yes", "action=unregister 시 확인 프롬프트 건너뛰기")
1747
1750
  .option("--format <type>", "출력 형식 (json|table)", "table")
1748
1751
  .action(async (options) => {
1749
1752
  try {
@@ -1989,8 +1992,64 @@ kbCmd
1989
1992
  process.exit(1);
1990
1993
  }
1991
1994
  }
1995
+ else if (action === "unregister") {
1996
+ if (!options.domain) {
1997
+ console.log(chalk_1.default.red("--domain 옵션이 필요합니다."));
1998
+ process.exit(1);
1999
+ }
2000
+ // 도메인 정보 조회
2001
+ const domainInfo = await (0, kb_1.ontoShow)(pool, options.domain);
2002
+ if (!domainInfo) {
2003
+ console.log(chalk_1.default.red(`\n❌ 도메인 '${options.domain}'은(는) 존재하지 않습니다.\n`));
2004
+ process.exit(1);
2005
+ }
2006
+ // KB 엔트리 수 확인
2007
+ const countRes = await pool.query("SELECT COUNT(*)::int AS cnt FROM semo.knowledge_base WHERE domain = $1", [options.domain]);
2008
+ const kbCount = countRes.rows[0].cnt;
2009
+ // 확인 프롬프트
2010
+ if (!options.yes) {
2011
+ const typeStr = domainInfo.entity_type ? ` [${domainInfo.entity_type}]` : "";
2012
+ const svcStr = domainInfo.service && domainInfo.service !== "_global" ? ` (${domainInfo.service})` : "";
2013
+ if (kbCount > 0 && options.force) {
2014
+ console.log(chalk_1.default.yellow(`\n⚠️ 도메인 '${options.domain}'에 KB 항목 ${kbCount}건이 남아있습니다.`));
2015
+ console.log(chalk_1.default.yellow(` --force 옵션으로 모두 삭제됩니다.\n`));
2016
+ }
2017
+ else {
2018
+ console.log(chalk_1.default.cyan(`\n📐 삭제 대상 도메인: ${options.domain}${typeStr}${svcStr}`));
2019
+ console.log(chalk_1.default.gray(` KB 항목: ${kbCount}건\n`));
2020
+ }
2021
+ const { createInterface } = await import("readline");
2022
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2023
+ const answer = await new Promise((resolve) => {
2024
+ rl.question(chalk_1.default.yellow(" 정말 삭제하시겠습니까? (y/N): "), resolve);
2025
+ });
2026
+ rl.close();
2027
+ if (answer.toLowerCase() !== "y") {
2028
+ console.log(chalk_1.default.gray(" 취소됨.\n"));
2029
+ await (0, database_1.closeConnection)();
2030
+ return;
2031
+ }
2032
+ }
2033
+ const result = await (0, kb_1.ontoUnregister)(pool, options.domain, !!options.force);
2034
+ if (options.format === "json") {
2035
+ console.log(JSON.stringify(result, null, 2));
2036
+ }
2037
+ else {
2038
+ if (result.success) {
2039
+ console.log(chalk_1.default.green(`\n✅ 도메인 '${options.domain}' 삭제 완료`));
2040
+ if (result.deleted_entries && result.deleted_entries > 0) {
2041
+ console.log(chalk_1.default.gray(` KB 항목 ${result.deleted_entries}건 함께 삭제됨`));
2042
+ }
2043
+ console.log();
2044
+ }
2045
+ else {
2046
+ console.log(chalk_1.default.red(`\n❌ 삭제 실패: ${result.error}\n`));
2047
+ process.exit(1);
2048
+ }
2049
+ }
2050
+ }
1992
2051
  else {
1993
- console.log(chalk_1.default.red(`알 수 없는 action: '${action}'. 사용 가능: list, show, services, types, instances, schema, routing-table, register, create-type, add-key, remove-key`));
2052
+ console.log(chalk_1.default.red(`알 수 없는 action: '${action}'. 사용 가능: list, show, services, types, instances, schema, routing-table, register, create-type, add-key, remove-key, unregister`));
1994
2053
  process.exit(1);
1995
2054
  }
1996
2055
  await (0, database_1.closeConnection)();
@@ -2218,6 +2277,74 @@ ontoCmd
2218
2277
  process.exit(1);
2219
2278
  }
2220
2279
  });
2280
+ ontoCmd
2281
+ .command("unregister <domain>")
2282
+ .description("온톨로지 도메인 삭제 (KB 데이터 포함)")
2283
+ .option("--force", "잔존 KB 항목이 있어도 모두 삭제 후 도메인 제거")
2284
+ .option("--yes", "확인 프롬프트 건너뛰기")
2285
+ .option("--format <type>", "출력 형식 (json|table)", "table")
2286
+ .action(async (domain, options) => {
2287
+ try {
2288
+ const pool = (0, database_1.getPool)();
2289
+ // 도메인 정보 조회
2290
+ const domainInfo = await (0, kb_1.ontoShow)(pool, domain);
2291
+ if (!domainInfo) {
2292
+ console.log(chalk_1.default.red(`\n❌ 도메인 '${domain}'은(는) 존재하지 않습니다.\n`));
2293
+ await (0, database_1.closeConnection)();
2294
+ process.exit(1);
2295
+ }
2296
+ // KB 엔트리 수 확인
2297
+ const countRes = await pool.query("SELECT COUNT(*)::int AS cnt FROM semo.knowledge_base WHERE domain = $1", [domain]);
2298
+ const kbCount = countRes.rows[0].cnt;
2299
+ // 확인 프롬프트
2300
+ if (!options.yes) {
2301
+ const typeStr = domainInfo.entity_type ? ` [${domainInfo.entity_type}]` : "";
2302
+ const svcStr = domainInfo.service && domainInfo.service !== "_global" ? ` (${domainInfo.service})` : "";
2303
+ if (kbCount > 0 && options.force) {
2304
+ console.log(chalk_1.default.yellow(`\n⚠️ 도메인 '${domain}'에 KB 항목 ${kbCount}건이 남아있습니다.`));
2305
+ console.log(chalk_1.default.yellow(` --force 옵션으로 모두 삭제됩니다.\n`));
2306
+ }
2307
+ else {
2308
+ console.log(chalk_1.default.cyan(`\n📐 삭제 대상 도메인: ${domain}${typeStr}${svcStr}`));
2309
+ console.log(chalk_1.default.gray(` KB 항목: ${kbCount}건\n`));
2310
+ }
2311
+ const { createInterface } = await import("readline");
2312
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2313
+ const answer = await new Promise((resolve) => {
2314
+ rl.question(chalk_1.default.yellow(" 정말 삭제하시겠습니까? (y/N): "), resolve);
2315
+ });
2316
+ rl.close();
2317
+ if (answer.toLowerCase() !== "y") {
2318
+ console.log(chalk_1.default.gray(" 취소됨.\n"));
2319
+ await (0, database_1.closeConnection)();
2320
+ return;
2321
+ }
2322
+ }
2323
+ const result = await (0, kb_1.ontoUnregister)(pool, domain, !!options.force);
2324
+ if (options.format === "json") {
2325
+ console.log(JSON.stringify(result, null, 2));
2326
+ }
2327
+ else {
2328
+ if (result.success) {
2329
+ console.log(chalk_1.default.green(`\n✅ 도메인 '${domain}' 삭제 완료`));
2330
+ if (result.deleted_entries && result.deleted_entries > 0) {
2331
+ console.log(chalk_1.default.gray(` KB 항목 ${result.deleted_entries}건 함께 삭제됨`));
2332
+ }
2333
+ console.log();
2334
+ }
2335
+ else {
2336
+ console.log(chalk_1.default.red(`\n❌ 삭제 실패: ${result.error}\n`));
2337
+ process.exit(1);
2338
+ }
2339
+ }
2340
+ await (0, database_1.closeConnection)();
2341
+ }
2342
+ catch (err) {
2343
+ console.error(chalk_1.default.red(`삭제 실패: ${err}`));
2344
+ await (0, database_1.closeConnection)();
2345
+ process.exit(1);
2346
+ }
2347
+ });
2221
2348
  // === 신규 v4 커맨드 그룹 등록 ===
2222
2349
  (0, context_1.registerContextCommands)(program);
2223
2350
  (0, bots_1.registerBotsCommands)(program);
@@ -2226,6 +2353,7 @@ ontoCmd
2226
2353
  (0, db_1.registerDbCommands)(program);
2227
2354
  (0, memory_1.registerMemoryCommands)(program);
2228
2355
  (0, test_1.registerTestCommands)(program);
2356
+ (0, commitments_1.registerCommitmentsCommands)(program);
2229
2357
  // === semo skills — DB 시딩 ===
2230
2358
  /**
2231
2359
  * SKILL.md frontmatter 파싱 (YAML 파서 없이 regex 기반)
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
  */
package/dist/kb.js CHANGED
@@ -62,6 +62,7 @@ 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;
66
67
  exports.ontoCreateType = ontoCreateType;
67
68
  exports.ontoRemoveKey = ontoRemoveKey;
@@ -1011,6 +1012,50 @@ async function ontoRegister(pool, opts) {
1011
1012
  client.release();
1012
1013
  }
1013
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
+ }
1014
1059
  /**
1015
1060
  * Add a key to a type schema
1016
1061
  */
@@ -267,7 +267,7 @@ function generateUserMd() {
267
267
  const content = `# User Profile
268
268
 
269
269
  > 이 파일은 사용자 프로필입니다.
270
- > \`semo kb get semicolon team/{name}\`으로 KB에서 가져오거나 직접 수정하세요.
270
+ > \`semo kb get {name} contact\`으로 KB에서 가져오거나 직접 수정하세요.
271
271
 
272
272
  ## 기본 정보
273
273
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semo-cli",
3
- "version": "4.10.0",
3
+ "version": "4.12.0",
4
4
  "description": "SEMO CLI - AI Agent Orchestration Framework Installer",
5
5
  "main": "dist/index.js",
6
6
  "bin": {