@team-semicolon/semo-cli 4.11.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";
@@ -2352,6 +2353,7 @@ ontoCmd
2352
2353
  (0, db_1.registerDbCommands)(program);
2353
2354
  (0, memory_1.registerMemoryCommands)(program);
2354
2355
  (0, test_1.registerTestCommands)(program);
2356
+ (0, commitments_1.registerCommitmentsCommands)(program);
2355
2357
  // === semo skills — DB 시딩 ===
2356
2358
  /**
2357
2359
  * SKILL.md frontmatter 파싱 (YAML 파서 없이 regex 기반)
@@ -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.11.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": {