seogitan 1.2.0 → 1.4.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.
@@ -1,8 +1,6 @@
1
1
  import * as http from "node:http";
2
2
  import { saveTokens } from "../lib/auth.js";
3
- // Supabase URL에서 프로젝트 ref 추출 → Vercel 배포 URL 추론
4
- // 또는 환경변수로 직접 지정 가능
5
- const WEB_URL = process.env.SEOGITAN_WEB_URL || "https://seogitan.vercel.app";
3
+ import { WEB_URL } from "../config.js";
6
4
  export function registerLoginCommand(program) {
7
5
  program
8
6
  .command("login")
@@ -1,4 +1,4 @@
1
- import { listMeetings, getMeeting, getMeetingSummary, getMeetingTranscripts, searchMeetings, } from "seogitan-core";
1
+ import { listMeetings, getMeeting, getMeetingSummary, getMeetingTranscripts, searchMeetings, deleteMeeting, updateMeetingTags, } from "seogitan-core";
2
2
  import { getSupabaseClient } from "../lib/supabase.js";
3
3
  import { formatMeetingsList, formatMeetingDetail, formatSearchResults, } from "../lib/output.js";
4
4
  import { EXIT_CODES } from "../exit-codes.js";
@@ -14,15 +14,23 @@ export function registerMeetingsCommand(program) {
14
14
  .option("--from <date>", "조회 시작 날짜 (ISO 8601)")
15
15
  .option("--to <date>", "조회 종료 날짜 (ISO 8601)")
16
16
  .option("--limit <n>", "조회할 최대 개수", "10")
17
+ .option("--sort <order>", "정렬 (newest|name)", "newest")
18
+ .option("--filter <type>", "필터 (mine|shared)")
19
+ .option("--tag <tag>", "태그로 필터링")
17
20
  .option("--json", "JSON 형식으로 출력", false)
18
21
  .action(async (opts) => {
19
22
  try {
20
23
  const supabase = await getSupabaseClient();
24
+ const { data: { user } } = await supabase.auth.getUser();
21
25
  const data = await listMeetings(supabase, {
22
26
  limit: parseInt(opts.limit, 10),
23
27
  status: opts.status,
24
28
  date_from: opts.from,
25
29
  date_to: opts.to,
30
+ sort: opts.sort,
31
+ ownership: opts.filter,
32
+ userId: user?.id,
33
+ tag: opts.tag,
26
34
  });
27
35
  console.log(formatMeetingsList(data, opts.json));
28
36
  }
@@ -157,4 +165,55 @@ export function registerMeetingsCommand(program) {
157
165
  process.exit(EXIT_CODES.ERROR);
158
166
  }
159
167
  });
168
+ // delete
169
+ meetings
170
+ .command("delete <id>")
171
+ .description("회의를 삭제합니다 (생성자만 가능)")
172
+ .option("--force", "확인 없이 삭제", false)
173
+ .action(async (id, opts) => {
174
+ try {
175
+ const supabase = await getSupabaseClient();
176
+ const meeting = await getMeeting(supabase, id);
177
+ if (!meeting) {
178
+ console.error(`회의를 찾을 수 없습니다: ${id}`);
179
+ process.exit(EXIT_CODES.NOT_FOUND);
180
+ }
181
+ if (!opts.force) {
182
+ const { createInterface } = await import("node:readline");
183
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
184
+ const answer = await new Promise((resolve) => rl.question(`"${meeting.title}" 회의를 삭제할까요? (y/N) `, resolve));
185
+ rl.close();
186
+ if (answer.toLowerCase() !== "y") {
187
+ console.log("삭제가 취소되었습니다.");
188
+ return;
189
+ }
190
+ }
191
+ await deleteMeeting(supabase, id);
192
+ console.log(`회의가 삭제되었습니다: ${meeting.title}`);
193
+ }
194
+ catch (err) {
195
+ console.error(`오류: ${err instanceof Error ? err.message : String(err)}`);
196
+ process.exit(EXIT_CODES.ERROR);
197
+ }
198
+ });
199
+ // tag
200
+ meetings
201
+ .command("tag <id> [tags...]")
202
+ .description("회의에 태그를 설정합니다")
203
+ .action(async (id, tags) => {
204
+ try {
205
+ const supabase = await getSupabaseClient();
206
+ const meeting = await getMeeting(supabase, id);
207
+ if (!meeting) {
208
+ console.error(`회의를 찾을 수 없습니다: ${id}`);
209
+ process.exit(EXIT_CODES.NOT_FOUND);
210
+ }
211
+ await updateMeetingTags(supabase, id, tags);
212
+ console.log(`태그 설정됨: ${tags.length > 0 ? tags.join(", ") : "(태그 삭제)"}`);
213
+ }
214
+ catch (err) {
215
+ console.error(`오류: ${err instanceof Error ? err.message : String(err)}`);
216
+ process.exit(EXIT_CODES.ERROR);
217
+ }
218
+ });
160
219
  }
@@ -5,6 +5,7 @@ import { getValidGoogleToken } from "../lib/calendar.js";
5
5
  import { checkFfmpegInstalled, listAudioDevices, createAudioRecorder, createTempOutputDir, } from "../lib/audio.js";
6
6
  import { triggerChunkTranscription } from "../lib/n8n.js";
7
7
  import { EXIT_CODES } from "../exit-codes.js";
8
+ import { WEB_URL } from "../config.js";
8
9
  import { fetchTodayEvents, linkCalendarEvent } from "seogitan-core";
9
10
  export function registerRecordCommand(program) {
10
11
  program
@@ -205,6 +206,7 @@ export function registerRecordCommand(program) {
205
206
  catch { }
206
207
  }
207
208
  console.log(`녹음 완료. Meeting ID: ${meetingId}`);
209
+ console.log(`웹에서 보기: ${WEB_URL}/meetings/${meetingId}`);
208
210
  }
209
211
  catch (err) {
210
212
  console.error(`정리 중 오류: ${err instanceof Error ? err.message : String(err)}`);
@@ -216,6 +218,7 @@ export function registerRecordCommand(program) {
216
218
  const chunkMinutes = Math.floor(chunkDurationSeconds / 60);
217
219
  console.log(`녹음 시작: ${tempTitle} (${chunkMinutes}분마다 자동 전사, Ctrl+C로 종료)`);
218
220
  console.log(`Meeting ID: ${meetingId}`);
221
+ console.log(`웹에서 보기: ${WEB_URL}/meetings/${meetingId}`);
219
222
  }
220
223
  catch (err) {
221
224
  console.error(`오류: ${err instanceof Error ? err.message : String(err)}`);
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export declare const SUPABASE_URL: string;
2
2
  export declare const SUPABASE_ANON_KEY: string;
3
+ export declare const WEB_URL: string;
3
4
  export declare const SUPABASE_FUNCTIONS_URL: string;
package/dist/config.js CHANGED
@@ -6,6 +6,8 @@ export const SUPABASE_URL = process.env.SUPABASE_URL ||
6
6
  export const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY ||
7
7
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
8
8
  "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBuZWR3cWJidXF2aXlrbmdwZGJpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzQ1NTc2NDYsImV4cCI6MjA5MDEzMzY0Nn0.cbLANAeWNy6XxL-dhOIdnGRV_C0c68lLM94H1-nk-fg";
9
+ // Web App URL
10
+ export const WEB_URL = process.env.SEOGITAN_WEB_URL || "https://seogitan.vercel.app";
9
11
  // Supabase Edge Functions URL
10
12
  export const SUPABASE_FUNCTIONS_URL = process.env.SUPABASE_FUNCTIONS_URL ||
11
13
  "https://pnedwqbbuqviykngpdbi.supabase.co/functions/v1";
package/dist/lib/audio.js CHANGED
@@ -26,13 +26,32 @@ export function checkFfmpegInstalled() {
26
26
  }
27
27
  }
28
28
  export function listAudioDevices() {
29
+ const ffmpeg = getFfmpegPath();
30
+ let cmd;
31
+ if (process.platform === "darwin") {
32
+ cmd = `"${ffmpeg}" -f avfoundation -list_devices true -i dummy`;
33
+ }
34
+ else if (process.platform === "win32") {
35
+ cmd = `"${ffmpeg}" -f dshow -list_devices true -i dummy`;
36
+ }
37
+ else {
38
+ // Linux: use pactl
39
+ try {
40
+ const out = execSync("pactl list short sources", { encoding: "utf-8" });
41
+ return out.split("\n").filter(Boolean).map((line, i) => ({
42
+ index: i,
43
+ name: line.split("\t")[1] || line,
44
+ }));
45
+ }
46
+ catch {
47
+ return [];
48
+ }
49
+ }
29
50
  try {
30
- // ffmpeg exits with error code when listing devices, so we must catch
31
- execSync(`"${getFfmpegPath()}" -f avfoundation -list_devices true -i dummy`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
51
+ execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
32
52
  return [];
33
53
  }
34
54
  catch (err) {
35
- // ffmpeg writes device list to stderr even on "error" exit
36
55
  if (err && typeof err === "object" && "stderr" in err) {
37
56
  return parseAudioDevices(err.stderr);
38
57
  }
@@ -72,9 +91,16 @@ export function createAudioRecorder(options) {
72
91
  let watcher = null;
73
92
  // Build ffmpeg args based on platform
74
93
  function buildFfmpegArgs() {
75
- const inputArgs = process.platform === "darwin"
76
- ? ["-f", "avfoundation", "-i", `:${deviceIndex}`]
77
- : ["-f", "pulse", "-i", "default"];
94
+ let inputArgs;
95
+ if (process.platform === "darwin") {
96
+ inputArgs = ["-f", "avfoundation", "-i", `:${deviceIndex}`];
97
+ }
98
+ else if (process.platform === "win32") {
99
+ inputArgs = ["-f", "dshow", "-i", `audio=${deviceIndex}`];
100
+ }
101
+ else {
102
+ inputArgs = ["-f", "pulse", "-i", "default"];
103
+ }
78
104
  return [
79
105
  ...inputArgs,
80
106
  "-ar",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seogitan",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "seogitan": "dist/index.js"
@@ -19,7 +19,7 @@
19
19
  "commander": "^13.0.0",
20
20
  "ffmpeg-static": "^5.3.0",
21
21
  "open": "^10.0.0",
22
- "seogitan-core": "^1.0.0",
22
+ "seogitan-core": "^1.1.0",
23
23
  "zod": "^3.24.3"
24
24
  },
25
25
  "devDependencies": {