@team-semicolon/semo-cli 4.0.5 → 4.1.1
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/commands/bots.js +5 -0
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +266 -0
- package/dist/index.js +141 -22
- package/package.json +1 -1
package/dist/commands/bots.js
CHANGED
|
@@ -330,6 +330,11 @@ function registerBotsCommands(program) {
|
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
332
|
await client.query("COMMIT");
|
|
333
|
+
// session_count를 bot_sessions 실제 집계로 갱신
|
|
334
|
+
await client.query(`UPDATE semo.bot_status bs
|
|
335
|
+
SET session_count = (
|
|
336
|
+
SELECT COUNT(*) FROM semo.bot_sessions WHERE bot_id = bs.bot_id
|
|
337
|
+
)`);
|
|
333
338
|
spinner.succeed(`bots sync 완료: ${upserted}개 봇 업서트`);
|
|
334
339
|
if (errors.length > 0) {
|
|
335
340
|
errors.forEach(e => console.log(chalk_1.default.red(` ❌ ${e}`)));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* semo sessions — 세션 추적
|
|
3
|
+
*
|
|
4
|
+
* Claude Code 훅(SessionStart / Stop)에서 stdin으로 전달되는 JSON을 파싱해
|
|
5
|
+
* semo.bot_sessions 테이블에 upsert합니다.
|
|
6
|
+
*
|
|
7
|
+
* Claude Code hook stdin 구조:
|
|
8
|
+
* SessionStart: { session_id, transcript_path, cwd, hook_event_name }
|
|
9
|
+
* Stop: { session_id, transcript_path, hook_event_name }
|
|
10
|
+
*
|
|
11
|
+
* 사용법 (settings.json hooks):
|
|
12
|
+
* SessionStart → semo sessions push --bot-id workclaw --event start
|
|
13
|
+
* Stop → semo sessions push --bot-id workclaw --event stop
|
|
14
|
+
*/
|
|
15
|
+
import { Command } from "commander";
|
|
16
|
+
export declare function registerSessionsCommands(program: Command): void;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* semo sessions — 세션 추적
|
|
4
|
+
*
|
|
5
|
+
* Claude Code 훅(SessionStart / Stop)에서 stdin으로 전달되는 JSON을 파싱해
|
|
6
|
+
* semo.bot_sessions 테이블에 upsert합니다.
|
|
7
|
+
*
|
|
8
|
+
* Claude Code hook stdin 구조:
|
|
9
|
+
* SessionStart: { session_id, transcript_path, cwd, hook_event_name }
|
|
10
|
+
* Stop: { session_id, transcript_path, hook_event_name }
|
|
11
|
+
*
|
|
12
|
+
* 사용법 (settings.json hooks):
|
|
13
|
+
* SessionStart → semo sessions push --bot-id workclaw --event start
|
|
14
|
+
* Stop → semo sessions push --bot-id workclaw --event stop
|
|
15
|
+
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
50
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
51
|
+
};
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.registerSessionsCommands = registerSessionsCommands;
|
|
54
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
55
|
+
const fs = __importStar(require("fs"));
|
|
56
|
+
const path = __importStar(require("path"));
|
|
57
|
+
const readline = __importStar(require("readline"));
|
|
58
|
+
const child_process_1 = require("child_process");
|
|
59
|
+
const database_1 = require("../database");
|
|
60
|
+
async function readStdin() {
|
|
61
|
+
// stdin이 TTY면 hook에서 호출된 게 아님 → 빈 객체 반환
|
|
62
|
+
if (process.stdin.isTTY)
|
|
63
|
+
return {};
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
let raw = "";
|
|
66
|
+
process.stdin.setEncoding("utf-8");
|
|
67
|
+
process.stdin.on("data", (chunk) => (raw += chunk));
|
|
68
|
+
process.stdin.on("end", () => {
|
|
69
|
+
try {
|
|
70
|
+
resolve(JSON.parse(raw));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
resolve({});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// 500ms 타임아웃 — stdin이 오지 않으면 그냥 진행
|
|
77
|
+
setTimeout(() => resolve({}), 500);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// ─── 현재 git 브랜치 (label용) ───────────────────────────────────────────────
|
|
81
|
+
function getGitBranch(cwd) {
|
|
82
|
+
try {
|
|
83
|
+
const dir = cwd || process.cwd();
|
|
84
|
+
return (0, child_process_1.execSync)("git rev-parse --abbrev-ref HEAD", {
|
|
85
|
+
cwd: dir,
|
|
86
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
87
|
+
timeout: 2000,
|
|
88
|
+
})
|
|
89
|
+
.toString()
|
|
90
|
+
.trim();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// ─── transcript.jsonl 메시지 수 카운트 ───────────────────────────────────────
|
|
97
|
+
async function countMessages(transcriptPath) {
|
|
98
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath))
|
|
99
|
+
return 0;
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
let count = 0;
|
|
102
|
+
const rl = readline.createInterface({
|
|
103
|
+
input: fs.createReadStream(transcriptPath),
|
|
104
|
+
crlfDelay: Infinity,
|
|
105
|
+
});
|
|
106
|
+
rl.on("line", (line) => {
|
|
107
|
+
if (!line.trim())
|
|
108
|
+
return;
|
|
109
|
+
try {
|
|
110
|
+
const obj = JSON.parse(line);
|
|
111
|
+
// role이 있는 메시지(user/assistant)만 카운트
|
|
112
|
+
if (obj.role === "user" || obj.role === "assistant")
|
|
113
|
+
count++;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// invalid line skip
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
rl.on("close", () => resolve(count));
|
|
120
|
+
rl.on("error", () => resolve(0));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// ─── Command registration ─────────────────────────────────────────────────────
|
|
124
|
+
function registerSessionsCommands(program) {
|
|
125
|
+
const sessionsCmd = program
|
|
126
|
+
.command("sessions")
|
|
127
|
+
.description("세션 추적 (Claude Code 훅 연동)");
|
|
128
|
+
// ── semo sessions push ───────────────────────────────────────────────────────
|
|
129
|
+
sessionsCmd
|
|
130
|
+
.command("push")
|
|
131
|
+
.description("현재 세션을 semo.bot_sessions에 기록 (훅에서 호출)")
|
|
132
|
+
.requiredOption("--bot-id <id>", "봇 ID (e.g. workclaw)")
|
|
133
|
+
.option("--event <type>", "이벤트 종류 (start|stop|heartbeat)", "heartbeat")
|
|
134
|
+
.option("--label <text>", "세션 라벨 (미지정 시 git 브랜치 자동 감지)")
|
|
135
|
+
.option("--kind <kind>", "세션 종류 (main|isolated)", "main")
|
|
136
|
+
.action(async (options) => {
|
|
137
|
+
const botId = options.botId;
|
|
138
|
+
const event = options.event;
|
|
139
|
+
// stdin에서 Claude Code hook JSON 읽기
|
|
140
|
+
const hook = await readStdin();
|
|
141
|
+
const sessionKey = hook.session_id ||
|
|
142
|
+
process.env.CLAUDE_SESSION_ID ||
|
|
143
|
+
`${botId}-${Date.now()}`;
|
|
144
|
+
const branch = getGitBranch(hook.cwd);
|
|
145
|
+
const label = options.label ||
|
|
146
|
+
branch ||
|
|
147
|
+
path.basename(hook.cwd || process.cwd());
|
|
148
|
+
const messageCount = event === "stop" && hook.transcript_path
|
|
149
|
+
? await countMessages(hook.transcript_path)
|
|
150
|
+
: undefined;
|
|
151
|
+
const connected = await (0, database_1.isDbConnected)();
|
|
152
|
+
if (!connected) {
|
|
153
|
+
// 훅에서 호출 시 조용히 실패 (봇 세션에 영향 주지 않도록)
|
|
154
|
+
await (0, database_1.closeConnection)();
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const pool = (0, database_1.getPool)();
|
|
159
|
+
const client = await pool.connect();
|
|
160
|
+
if (event === "start") {
|
|
161
|
+
await client.query(`INSERT INTO semo.bot_sessions
|
|
162
|
+
(bot_id, session_key, label, kind, chat_type, last_activity, message_count, synced_at)
|
|
163
|
+
VALUES ($1, $2, $3, $4, 'claude-code', NOW(), 0, NOW())
|
|
164
|
+
ON CONFLICT (bot_id, session_key) DO UPDATE SET
|
|
165
|
+
label = EXCLUDED.label,
|
|
166
|
+
last_activity = NOW(),
|
|
167
|
+
synced_at = NOW()`, [botId, sessionKey, label, options.kind]);
|
|
168
|
+
// bot_status.session_count 갱신
|
|
169
|
+
await client.query(`UPDATE semo.bot_status
|
|
170
|
+
SET session_count = (
|
|
171
|
+
SELECT COUNT(*) FROM semo.bot_sessions WHERE bot_id = $1
|
|
172
|
+
),
|
|
173
|
+
synced_at = NOW()
|
|
174
|
+
WHERE bot_id = $1`, [botId]);
|
|
175
|
+
}
|
|
176
|
+
else if (event === "stop") {
|
|
177
|
+
await client.query(`UPDATE semo.bot_sessions
|
|
178
|
+
SET last_activity = NOW(),
|
|
179
|
+
message_count = COALESCE($1, message_count),
|
|
180
|
+
synced_at = NOW()
|
|
181
|
+
WHERE bot_id = $2 AND session_key = $3`, [messageCount ?? null, botId, sessionKey]);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// heartbeat — 마지막 활동 시간 + 메시지 수 갱신
|
|
185
|
+
await client.query(`INSERT INTO semo.bot_sessions
|
|
186
|
+
(bot_id, session_key, label, kind, chat_type, last_activity, message_count, synced_at)
|
|
187
|
+
VALUES ($1, $2, $3, $4, 'claude-code', NOW(), COALESCE($5, 0), NOW())
|
|
188
|
+
ON CONFLICT (bot_id, session_key) DO UPDATE SET
|
|
189
|
+
last_activity = NOW(),
|
|
190
|
+
message_count = COALESCE(EXCLUDED.message_count, semo.bot_sessions.message_count),
|
|
191
|
+
synced_at = NOW()`, [botId, sessionKey, label, options.kind, messageCount ?? null]);
|
|
192
|
+
}
|
|
193
|
+
client.release();
|
|
194
|
+
console.log(chalk_1.default.green(`✔ sessions push [${event}] ${botId}/${sessionKey.slice(0, 8)}`));
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
// 훅에서 호출 시 조용히 실패
|
|
198
|
+
console.error(chalk_1.default.red(`sessions push 실패: ${err}`));
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
await (0, database_1.closeConnection)();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
// ── semo sessions list ───────────────────────────────────────────────────────
|
|
206
|
+
sessionsCmd
|
|
207
|
+
.command("list")
|
|
208
|
+
.description("bot_sessions 테이블 조회")
|
|
209
|
+
.option("--bot-id <id>", "특정 봇만")
|
|
210
|
+
.option("--limit <n>", "최대 조회 수", "20")
|
|
211
|
+
.option("--format <type>", "출력 형식 (table|json)", "table")
|
|
212
|
+
.action(async (options) => {
|
|
213
|
+
const connected = await (0, database_1.isDbConnected)();
|
|
214
|
+
if (!connected) {
|
|
215
|
+
console.log(chalk_1.default.red("❌ DB 연결 실패"));
|
|
216
|
+
await (0, database_1.closeConnection)();
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const pool = (0, database_1.getPool)();
|
|
221
|
+
const client = await pool.connect();
|
|
222
|
+
const params = [];
|
|
223
|
+
let where = "";
|
|
224
|
+
if (options.botId) {
|
|
225
|
+
where = "WHERE bot_id = $1";
|
|
226
|
+
params.push(options.botId);
|
|
227
|
+
}
|
|
228
|
+
params.push(parseInt(options.limit));
|
|
229
|
+
const limitIdx = params.length;
|
|
230
|
+
const result = await client.query(`SELECT bot_id, session_key, label, kind, chat_type,
|
|
231
|
+
last_activity::text, message_count
|
|
232
|
+
FROM semo.bot_sessions
|
|
233
|
+
${where}
|
|
234
|
+
ORDER BY last_activity DESC NULLS LAST
|
|
235
|
+
LIMIT $${limitIdx}`, params);
|
|
236
|
+
client.release();
|
|
237
|
+
if (options.format === "json") {
|
|
238
|
+
console.log(JSON.stringify(result.rows, null, 2));
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
console.log(chalk_1.default.cyan.bold("\n📋 세션 목록\n"));
|
|
242
|
+
if (result.rows.length === 0) {
|
|
243
|
+
console.log(chalk_1.default.yellow(" 세션 없음"));
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
for (const s of result.rows) {
|
|
247
|
+
const ts = s.last_activity
|
|
248
|
+
? new Date(s.last_activity).toLocaleString("ko-KR")
|
|
249
|
+
: "-";
|
|
250
|
+
console.log(chalk_1.default.cyan(` ${s.bot_id.padEnd(14)}`) +
|
|
251
|
+
chalk_1.default.white(`${(s.label || s.session_key).padEnd(30)}`) +
|
|
252
|
+
chalk_1.default.gray(`${ts} ${s.message_count}msg`));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
console.log();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
console.log(chalk_1.default.red(`❌ 조회 실패: ${err}`));
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
await (0, database_1.closeConnection)();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -60,6 +60,7 @@ const database_1 = require("./database");
|
|
|
60
60
|
const context_1 = require("./commands/context");
|
|
61
61
|
const bots_1 = require("./commands/bots");
|
|
62
62
|
const get_1 = require("./commands/get");
|
|
63
|
+
const sessions_1 = require("./commands/sessions");
|
|
63
64
|
const PACKAGE_NAME = "@team-semicolon/semo-cli";
|
|
64
65
|
// package.json에서 버전 동적 로드
|
|
65
66
|
function getCliVersion() {
|
|
@@ -772,6 +773,7 @@ program
|
|
|
772
773
|
.option("--no-gitignore", ".gitignore 수정 생략")
|
|
773
774
|
.option("--migrate", "레거시 환경 강제 마이그레이션")
|
|
774
775
|
.option("--seed-skills", "semo-system/semo-skills/ → semo.skills DB 초기 시딩")
|
|
776
|
+
.option("--credentials-gist <gistId>", "Private GitHub Gist에서 팀 DB 접속정보 자동 가져오기")
|
|
775
777
|
.action(async (options) => {
|
|
776
778
|
console.log(chalk_1.default.cyan.bold("\n🚀 SEMO 설치 시작\n"));
|
|
777
779
|
console.log(chalk_1.default.gray("Gemini 하이브리드 전략: White Box + Black Box\n"));
|
|
@@ -822,8 +824,8 @@ program
|
|
|
822
824
|
}
|
|
823
825
|
// 7. Hooks 설치 (대화 로깅)
|
|
824
826
|
await setupHooks(cwd, false);
|
|
825
|
-
// 7.5. ~/.semo.env
|
|
826
|
-
|
|
827
|
+
// 7.5. ~/.semo.env DB 접속 설정 (자동 감지 → Gist → 프롬프트)
|
|
828
|
+
await setupSemoEnv(options.credentialsGist);
|
|
827
829
|
// 8. CLAUDE.md 생성
|
|
828
830
|
await setupClaudeMd(cwd, [], options.force);
|
|
829
831
|
// 9. 설치 검증
|
|
@@ -849,11 +851,11 @@ program
|
|
|
849
851
|
console.log(chalk_1.default.gray(" ✓ semo-agents (14개 페르소나 Agent)"));
|
|
850
852
|
console.log(chalk_1.default.gray(" ✓ semo-scripts (자동화 스크립트)"));
|
|
851
853
|
console.log(chalk_1.default.cyan("\n다음 단계:"));
|
|
852
|
-
console.log(chalk_1.default.gray(" 1.
|
|
853
|
-
console.log(chalk_1.default.gray("
|
|
854
|
-
console.log(chalk_1.default.gray("
|
|
855
|
-
console.log(
|
|
856
|
-
console.log(chalk_1.default.gray("
|
|
854
|
+
console.log(chalk_1.default.gray(" 1. Claude Code에서 프로젝트 열기 (SessionStart 훅이 자동 sync)"));
|
|
855
|
+
console.log(chalk_1.default.gray(" 2. 자연어로 요청하기 (예: \"댓글 기능 구현해줘\")"));
|
|
856
|
+
console.log(chalk_1.default.gray(" 3. /SEMO:help로 도움말 확인"));
|
|
857
|
+
console.log();
|
|
858
|
+
console.log(chalk_1.default.gray(" DB 접속정보 변경: nano ~/.semo.env"));
|
|
857
859
|
console.log();
|
|
858
860
|
});
|
|
859
861
|
// === Standard 설치 (DB 기반) ===
|
|
@@ -1385,30 +1387,93 @@ const BASE_MCP_SERVERS = [
|
|
|
1385
1387
|
args: ["-y", "@modelcontextprotocol/server-github"],
|
|
1386
1388
|
},
|
|
1387
1389
|
];
|
|
1388
|
-
// === ~/.semo.env
|
|
1389
|
-
function
|
|
1390
|
+
// === ~/.semo.env 설정 (자동 감지 → Gist → 프롬프트) ===
|
|
1391
|
+
function writeSemoEnvFile(dbUrl, slackWebhook = "") {
|
|
1390
1392
|
const envFile = path.join(os.homedir(), ".semo.env");
|
|
1391
|
-
|
|
1392
|
-
return; // 이미 존재하면 건너뜀
|
|
1393
|
-
const template = `# SEMO 환경변수 — 모든 컨텍스트에서 자동 로드됨
|
|
1393
|
+
const content = `# SEMO 환경변수 — 모든 컨텍스트에서 자동 로드됨
|
|
1394
1394
|
# (Claude Code 앱, OpenClaw LaunchAgent, cron 등 비인터랙티브 환경 포함)
|
|
1395
|
-
#
|
|
1396
|
-
# 팀 Core DB 접속 정보를 여기에 입력하세요.
|
|
1397
|
-
# 설정 후 Claude Code 세션을 재시작하면 자동으로 context sync가 동작합니다.
|
|
1398
1395
|
|
|
1399
|
-
DATABASE_URL=''
|
|
1396
|
+
DATABASE_URL='${dbUrl}'
|
|
1400
1397
|
|
|
1401
1398
|
# Slack 알림 Webhook (선택 — bot-ops 채널 dead-letter 감지용)
|
|
1402
|
-
SLACK_WEBHOOK=''
|
|
1399
|
+
SLACK_WEBHOOK='${slackWebhook}'
|
|
1403
1400
|
`;
|
|
1401
|
+
fs.writeFileSync(envFile, content, { mode: 0o600 });
|
|
1402
|
+
}
|
|
1403
|
+
function readSemoEnvDbUrl() {
|
|
1404
|
+
const envFile = path.join(os.homedir(), ".semo.env");
|
|
1405
|
+
if (!fs.existsSync(envFile))
|
|
1406
|
+
return null;
|
|
1407
|
+
const content = fs.readFileSync(envFile, "utf-8");
|
|
1408
|
+
const match = content.match(/^DATABASE_URL='([^']+)'/m) || content.match(/^DATABASE_URL="([^"]+)"/m);
|
|
1409
|
+
return match ? match[1] : null;
|
|
1410
|
+
}
|
|
1411
|
+
function fetchDbUrlFromGist(gistId) {
|
|
1404
1412
|
try {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1413
|
+
const raw = (0, child_process_1.execSync)(`gh gist view ${gistId} --raw`, {
|
|
1414
|
+
encoding: "utf-8",
|
|
1415
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1416
|
+
timeout: 8000,
|
|
1417
|
+
});
|
|
1418
|
+
// Gist 파일 형식: DATABASE_URL=<url> 또는 단순 URL
|
|
1419
|
+
const match = raw.match(/DATABASE_URL=['"]?([^'"\s]+)['"]?/) || raw.match(/^(postgres(?:ql)?:\/\/[^\s]+)/m);
|
|
1420
|
+
return match ? match[1].trim() : null;
|
|
1409
1421
|
}
|
|
1410
1422
|
catch {
|
|
1411
|
-
|
|
1423
|
+
return null;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
async function setupSemoEnv(credentialsGist) {
|
|
1427
|
+
const envFile = path.join(os.homedir(), ".semo.env");
|
|
1428
|
+
console.log(chalk_1.default.cyan("\n🔑 DB 접속 설정"));
|
|
1429
|
+
// 1. 이미 env var로 설정됨
|
|
1430
|
+
if (process.env.DATABASE_URL) {
|
|
1431
|
+
console.log(chalk_1.default.green(" ✅ DATABASE_URL (환경변수)"));
|
|
1432
|
+
if (!readSemoEnvDbUrl()) {
|
|
1433
|
+
writeSemoEnvFile(process.env.DATABASE_URL);
|
|
1434
|
+
console.log(chalk_1.default.gray(` → ~/.semo.env 에 저장됨 (비인터랙티브 환경용)`));
|
|
1435
|
+
}
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
// 2. ~/.semo.env 에 이미 있음
|
|
1439
|
+
const existing = readSemoEnvDbUrl();
|
|
1440
|
+
if (existing) {
|
|
1441
|
+
console.log(chalk_1.default.green(" ✅ DATABASE_URL (~/.semo.env)"));
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
// 3. Private GitHub Gist 자동 fetch
|
|
1445
|
+
const gistId = credentialsGist || process.env.SEMO_CREDENTIALS_GIST;
|
|
1446
|
+
if (gistId) {
|
|
1447
|
+
console.log(chalk_1.default.gray(" GitHub Gist에서 팀 접속정보 가져오는 중..."));
|
|
1448
|
+
const url = fetchDbUrlFromGist(gistId);
|
|
1449
|
+
if (url) {
|
|
1450
|
+
writeSemoEnvFile(url);
|
|
1451
|
+
console.log(chalk_1.default.green(` ✅ Gist (${gistId.slice(0, 8)}...)에서 DATABASE_URL 설정됨`));
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
console.log(chalk_1.default.yellow(" ⚠️ Gist fetch 실패 — 수동 입력으로 전환"));
|
|
1455
|
+
}
|
|
1456
|
+
// 4. 인터랙티브 프롬프트
|
|
1457
|
+
console.log(chalk_1.default.gray(" 팀 Core DB URL을 붙여넣으세요 (나중에 ~/.semo.env에서 수정 가능)"));
|
|
1458
|
+
const { dbUrl } = await inquirer_1.default.prompt([
|
|
1459
|
+
{
|
|
1460
|
+
type: "password",
|
|
1461
|
+
name: "dbUrl",
|
|
1462
|
+
message: "DATABASE_URL:",
|
|
1463
|
+
mask: "*",
|
|
1464
|
+
},
|
|
1465
|
+
]);
|
|
1466
|
+
if (dbUrl && dbUrl.trim()) {
|
|
1467
|
+
writeSemoEnvFile(dbUrl.trim());
|
|
1468
|
+
console.log(chalk_1.default.green(" ✅ ~/.semo.env 저장됨 (권한: 600)"));
|
|
1469
|
+
console.log(chalk_1.default.gray(` 파일: ${envFile}`));
|
|
1470
|
+
}
|
|
1471
|
+
else {
|
|
1472
|
+
// 빈 템플릿 생성
|
|
1473
|
+
if (!fs.existsSync(envFile)) {
|
|
1474
|
+
writeSemoEnvFile("");
|
|
1475
|
+
}
|
|
1476
|
+
console.log(chalk_1.default.yellow(" ⚠️ 건너뜀 — ~/.semo.env에서 DATABASE_URL을 직접 입력하세요"));
|
|
1412
1477
|
}
|
|
1413
1478
|
}
|
|
1414
1479
|
// === Claude MCP 서버 존재 여부 확인 ===
|
|
@@ -2309,6 +2374,59 @@ program
|
|
|
2309
2374
|
console.log(chalk_1.default.cyan("\n새 환경 설치를 위해 'semo init'을 실행하세요.\n"));
|
|
2310
2375
|
}
|
|
2311
2376
|
});
|
|
2377
|
+
// === config 명령어 (설치 후 설정 변경) ===
|
|
2378
|
+
const configCmd = program.command("config").description("SEMO 설정 관리");
|
|
2379
|
+
configCmd
|
|
2380
|
+
.command("db")
|
|
2381
|
+
.description("팀 Core DB 접속정보 설정 (DATABASE_URL → ~/.semo.env)")
|
|
2382
|
+
.option("--credentials-gist <gistId>", "Private GitHub Gist에서 자동 가져오기")
|
|
2383
|
+
.action(async (options) => {
|
|
2384
|
+
console.log(chalk_1.default.cyan.bold("\n🔑 DB 접속정보 설정\n"));
|
|
2385
|
+
// 기존 값 확인
|
|
2386
|
+
const existing = readSemoEnvDbUrl();
|
|
2387
|
+
if (existing) {
|
|
2388
|
+
const { overwrite } = await inquirer_1.default.prompt([
|
|
2389
|
+
{
|
|
2390
|
+
type: "confirm",
|
|
2391
|
+
name: "overwrite",
|
|
2392
|
+
message: `기존 DATABASE_URL이 있습니다. 덮어쓰시겠습니까?`,
|
|
2393
|
+
default: false,
|
|
2394
|
+
},
|
|
2395
|
+
]);
|
|
2396
|
+
if (!overwrite) {
|
|
2397
|
+
console.log(chalk_1.default.gray("취소됨"));
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
// Gist 또는 프롬프트로 가져오기
|
|
2402
|
+
const gistId = options.credentialsGist || process.env.SEMO_CREDENTIALS_GIST;
|
|
2403
|
+
if (gistId) {
|
|
2404
|
+
console.log(chalk_1.default.gray("GitHub Gist에서 접속정보 가져오는 중..."));
|
|
2405
|
+
const url = fetchDbUrlFromGist(gistId);
|
|
2406
|
+
if (url) {
|
|
2407
|
+
writeSemoEnvFile(url);
|
|
2408
|
+
console.log(chalk_1.default.green("✅ ~/.semo.env 업데이트 완료"));
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
console.log(chalk_1.default.yellow("⚠️ Gist fetch 실패 — 수동 입력"));
|
|
2412
|
+
}
|
|
2413
|
+
const { dbUrl } = await inquirer_1.default.prompt([
|
|
2414
|
+
{
|
|
2415
|
+
type: "password",
|
|
2416
|
+
name: "dbUrl",
|
|
2417
|
+
message: "DATABASE_URL:",
|
|
2418
|
+
mask: "*",
|
|
2419
|
+
},
|
|
2420
|
+
]);
|
|
2421
|
+
if (dbUrl && dbUrl.trim()) {
|
|
2422
|
+
writeSemoEnvFile(dbUrl.trim());
|
|
2423
|
+
console.log(chalk_1.default.green("✅ ~/.semo.env 저장됨 (권한: 600)"));
|
|
2424
|
+
console.log(chalk_1.default.gray(" 다음 Claude Code 세션부터 자동으로 적용됩니다."));
|
|
2425
|
+
}
|
|
2426
|
+
else {
|
|
2427
|
+
console.log(chalk_1.default.yellow("취소됨"));
|
|
2428
|
+
}
|
|
2429
|
+
});
|
|
2312
2430
|
// === doctor 명령어 (설치 상태 진단) ===
|
|
2313
2431
|
program
|
|
2314
2432
|
.command("doctor")
|
|
@@ -2884,6 +3002,7 @@ ontoCmd
|
|
|
2884
3002
|
(0, context_1.registerContextCommands)(program);
|
|
2885
3003
|
(0, bots_1.registerBotsCommands)(program);
|
|
2886
3004
|
(0, get_1.registerGetCommands)(program);
|
|
3005
|
+
(0, sessions_1.registerSessionsCommands)(program);
|
|
2887
3006
|
// === semo skills — DB 시딩 ===
|
|
2888
3007
|
/**
|
|
2889
3008
|
* SKILL.md frontmatter 파싱 (YAML 파서 없이 regex 기반)
|