gencow 0.1.7 → 0.1.8

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.
Files changed (116) hide show
  1. package/bin/gencow.mjs +709 -33
  2. package/dashboard/404/index.html +1 -1
  3. package/dashboard/404.html +1 -1
  4. package/dashboard/__next.__PAGE__.txt +2 -2
  5. package/dashboard/__next._full.txt +12 -12
  6. package/dashboard/__next._head.txt +4 -4
  7. package/dashboard/__next._index.txt +7 -7
  8. package/dashboard/__next._tree.txt +1 -1
  9. package/dashboard/_next/static/chunks/260e9d64f07248c4.js +1 -0
  10. package/dashboard/_next/static/chunks/{33074d3ff5d0cd2d.js → 29f56225c1ab40ff.js} +1 -1
  11. package/dashboard/_next/static/chunks/35f92271baf1e273.js +2 -0
  12. package/dashboard/_next/static/chunks/46e5e8d7b7abb32f.js +1 -0
  13. package/dashboard/_next/static/chunks/544729b1d14e133f.js +1 -0
  14. package/dashboard/_next/static/chunks/5b9b63f901e3a179.js +1 -0
  15. package/dashboard/_next/static/chunks/5f1dc5123f43f954.js +1 -0
  16. package/dashboard/_next/static/chunks/69c2a0d587153425.js +1 -0
  17. package/dashboard/_next/static/chunks/6dd7133fff916008.js +1 -0
  18. package/dashboard/_next/static/chunks/6fa4d7b0382eb69a.js +4 -0
  19. package/dashboard/_next/static/chunks/{9fe49c7e68147829.js → 6faf0a47507abee5.js} +1 -1
  20. package/dashboard/_next/static/chunks/{c21e3239bd705e2c.js → 806eb71c7bda3038.js} +1 -1
  21. package/dashboard/_next/static/chunks/8157631bf108576e.js +1 -0
  22. package/dashboard/_next/static/chunks/967d50dc43e363ee.js +1 -0
  23. package/dashboard/_next/static/chunks/a6d4e29fef94fdae.js +1 -0
  24. package/dashboard/_next/static/chunks/a6dad97d9634a72d.js.map +1 -1
  25. package/dashboard/_next/static/chunks/{90c074f7c3ac4f8f.js → bf1d0c48a304cdf5.js} +1 -1
  26. package/dashboard/_next/static/chunks/{b929becb73e54029.js → c0d4e7aa4b1e8cd6.js} +23 -23
  27. package/dashboard/_next/static/chunks/{c8e3802643f7a4fb.js → ca7b9255bef4d853.js} +1 -1
  28. package/dashboard/_next/static/chunks/{34d22de4090e001f.js → d8490871b9f9d51f.js} +1 -1
  29. package/dashboard/_next/static/chunks/{turbopack-e4c70080f9797654.js → turbopack-00aed88e1fb5f17d.js} +1 -1
  30. package/dashboard/_not-found/__next._full.txt +12 -12
  31. package/dashboard/_not-found/__next._head.txt +4 -4
  32. package/dashboard/_not-found/__next._index.txt +7 -7
  33. package/dashboard/_not-found/__next._not-found.__PAGE__.txt +2 -2
  34. package/dashboard/_not-found/__next._not-found.txt +3 -3
  35. package/dashboard/_not-found/__next._tree.txt +1 -1
  36. package/dashboard/_not-found/index.html +1 -1
  37. package/dashboard/_not-found/index.txt +12 -12
  38. package/dashboard/auth/__next._full.txt +20 -17
  39. package/dashboard/auth/__next._head.txt +4 -4
  40. package/dashboard/auth/__next._index.txt +7 -7
  41. package/dashboard/auth/__next._tree.txt +1 -1
  42. package/dashboard/auth/__next.auth.__PAGE__.txt +8 -5
  43. package/dashboard/auth/__next.auth.txt +3 -3
  44. package/dashboard/auth/index.html +1 -1
  45. package/dashboard/auth/index.txt +20 -17
  46. package/dashboard/data/__next._full.txt +20 -17
  47. package/dashboard/data/__next._head.txt +4 -4
  48. package/dashboard/data/__next._index.txt +7 -7
  49. package/dashboard/data/__next._tree.txt +1 -1
  50. package/dashboard/data/__next.data.__PAGE__.txt +8 -5
  51. package/dashboard/data/__next.data.txt +3 -3
  52. package/dashboard/data/index.html +1 -1
  53. package/dashboard/data/index.txt +20 -17
  54. package/dashboard/files/__next._full.txt +13 -13
  55. package/dashboard/files/__next._head.txt +4 -4
  56. package/dashboard/files/__next._index.txt +7 -7
  57. package/dashboard/files/__next._tree.txt +1 -1
  58. package/dashboard/files/__next.files.__PAGE__.txt +3 -3
  59. package/dashboard/files/__next.files.txt +3 -3
  60. package/dashboard/files/index.html +1 -1
  61. package/dashboard/files/index.txt +13 -13
  62. package/dashboard/functions/__next._full.txt +13 -13
  63. package/dashboard/functions/__next._head.txt +4 -4
  64. package/dashboard/functions/__next._index.txt +7 -7
  65. package/dashboard/functions/__next._tree.txt +1 -1
  66. package/dashboard/functions/__next.functions.__PAGE__.txt +3 -3
  67. package/dashboard/functions/__next.functions.txt +3 -3
  68. package/dashboard/functions/index.html +1 -1
  69. package/dashboard/functions/index.txt +13 -13
  70. package/dashboard/index.html +1 -1
  71. package/dashboard/index.txt +12 -12
  72. package/dashboard/logs/__next._full.txt +13 -13
  73. package/dashboard/logs/__next._head.txt +4 -4
  74. package/dashboard/logs/__next._index.txt +7 -7
  75. package/dashboard/logs/__next._tree.txt +1 -1
  76. package/dashboard/logs/__next.logs.__PAGE__.txt +3 -3
  77. package/dashboard/logs/__next.logs.txt +3 -3
  78. package/dashboard/logs/index.html +1 -1
  79. package/dashboard/logs/index.txt +13 -13
  80. package/dashboard/scheduler/__next._full.txt +13 -13
  81. package/dashboard/scheduler/__next._head.txt +4 -4
  82. package/dashboard/scheduler/__next._index.txt +7 -7
  83. package/dashboard/scheduler/__next._tree.txt +1 -1
  84. package/dashboard/scheduler/__next.scheduler.__PAGE__.txt +3 -3
  85. package/dashboard/scheduler/__next.scheduler.txt +3 -3
  86. package/dashboard/scheduler/index.html +1 -1
  87. package/dashboard/scheduler/index.txt +13 -13
  88. package/dashboard/settings/__next._full.txt +14 -14
  89. package/dashboard/settings/__next._head.txt +4 -4
  90. package/dashboard/settings/__next._index.txt +7 -7
  91. package/dashboard/settings/__next._tree.txt +1 -1
  92. package/dashboard/settings/__next.settings.__PAGE__.txt +4 -4
  93. package/dashboard/settings/__next.settings.txt +3 -3
  94. package/dashboard/settings/index.html +1 -1
  95. package/dashboard/settings/index.txt +14 -14
  96. package/package.json +28 -29
  97. package/server/index.js +117 -0
  98. package/server/index.js.map +2 -2
  99. package/templates/ai.ts +154 -1
  100. package/templates/guardrails.ts +150 -0
  101. package/templates/parsers.ts +66 -0
  102. package/templates/prompts.ts +67 -0
  103. package/templates/rag.ts +188 -15
  104. package/templates/reranker.ts +95 -0
  105. package/dashboard/_next/static/chunks/1aa11153294820b1.js +0 -2
  106. package/dashboard/_next/static/chunks/3caf31dfdffb8e2e.js +0 -1
  107. package/dashboard/_next/static/chunks/49781d4788479f87.js +0 -4
  108. package/dashboard/_next/static/chunks/4f24965710e8e4cf.js +0 -1
  109. package/dashboard/_next/static/chunks/59607c439bb52384.js +0 -1
  110. package/dashboard/_next/static/chunks/8782022a9c17db71.js +0 -1
  111. package/dashboard/_next/static/chunks/8b72cfc40036c827.js +0 -1
  112. package/dashboard/_next/static/chunks/c332b20d9c29ae24.js +0 -1
  113. package/dashboard/_next/static/chunks/c4d90098b4abc498.js +0 -1
  114. /package/dashboard/_next/static/{2JoiCV-utsniJgNPfseeo → 7pe-AOGjyxk4S2bX5nhkB}/_buildManifest.js +0 -0
  115. /package/dashboard/_next/static/{2JoiCV-utsniJgNPfseeo → 7pe-AOGjyxk4S2bX5nhkB}/_clientMiddlewareManifest.json +0 -0
  116. /package/dashboard/_next/static/{2JoiCV-utsniJgNPfseeo → 7pe-AOGjyxk4S2bX5nhkB}/_ssgManifest.js +0 -0
package/bin/gencow.mjs CHANGED
@@ -1185,16 +1185,16 @@ ${BOLD}Dev commands:${RESET}
1185
1185
  ${GREEN}mcp${RESET} Start MCP server for AI agent integration ${DIM}(stdio)${RESET}
1186
1186
 
1187
1187
  ${BOLD}BaaS commands (login required):${RESET}
1188
- ${GREEN}login${RESET} Login to Gencow Platform
1188
+ ${GREEN}login${RESET} Login to Gencow Platform ${DIM}(browser → token)${RESET}
1189
1189
  ${GREEN}logout${RESET} Clear credentials
1190
- ${GREEN}dev:remote${RESET} Watch gencow/ + auto-deploy on change ${DIM}(like npx convex dev)${RESET}
1191
- ${GREEN}app list${RESET} List your apps
1192
- ${GREEN}app create <name>${RESET} Create a new app (provisions Bun process)
1193
- ${GREEN}app delete <name>${RESET} Delete an app
1194
- ${GREEN}app status <name>${RESET} Show app status
1195
- ${GREEN}remote:deploy${RESET} Bundle gencow/ and deploy to platform (one-shot)
1196
- ${GREEN}logs${RESET} Show recent logs ${DIM}(--follow for live streaming)${RESET}
1197
- ${GREEN}platform${RESET} Start the Control Plane server (port 4000)
1190
+ ${GREEN}whoami${RESET} Show current user info
1191
+ ${GREEN}deploy${RESET} Bundle gencow/ and deploy to platform
1192
+ ${DIM}--prod Deploy to production (confirmation required)${RESET}
1193
+ ${DIM}--name, -n Specify app name${RESET}
1194
+ ${GREEN}env list${RESET} List remote env vars ${DIM}(--prod for production)${RESET}
1195
+ ${GREEN}env set K=V${RESET} Set remote env var
1196
+ ${GREEN}env unset KEY${RESET} Remove remote env var
1197
+ ${GREEN}env push${RESET} Push local .env to remote
1198
1198
 
1199
1199
  ${BOLD}Production:${RESET}
1200
1200
  ${GREEN}deploy${RESET} Build Docker image + start production stack
@@ -1211,8 +1211,488 @@ ${BOLD}Examples:${RESET}
1211
1211
  `);
1212
1212
  },
1213
1213
 
1214
- // ── login ──────────────────────────────────────────
1215
- // ── mcp ──────────────────────────────────────────────
1214
+ // ── login — Device Auth 패턴 ──────────────────────────
1215
+ async login() {
1216
+ log(`\n${BOLD}${CYAN}Gencow Login${RESET}\n`);
1217
+
1218
+ const platformUrl = process.env.GENCOW_PLATFORM_URL || "https://gencow.dev";
1219
+ info(`Platform: ${platformUrl}`);
1220
+
1221
+ // 1. auth-start → 인증 코드 생성
1222
+ info("인증 코드 요청 중...");
1223
+ let authData;
1224
+ try {
1225
+ const res = await fetch(`${platformUrl}/platform/cli/auth-start`, {
1226
+ method: "POST",
1227
+ headers: { "Content-Type": "application/json" },
1228
+ });
1229
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1230
+ authData = await res.json();
1231
+ } catch (e) {
1232
+ error(`Platform 서버 연결 실패: ${e.message}`);
1233
+ error(`Platform URL: ${platformUrl}`);
1234
+ error(`서버가 실행 중인지 확인하세요.`);
1235
+ process.exit(1);
1236
+ }
1237
+
1238
+ log("");
1239
+ log(` ${BOLD}인증 코드: ${CYAN}${authData.code}${RESET}`);
1240
+ log("");
1241
+ info(`브라우저에서 로그인하세요:`);
1242
+ log(` ${CYAN}${authData.url}${RESET}`);
1243
+ log("");
1244
+
1245
+ // 2. 브라우저 오픈
1246
+ try {
1247
+ const { exec } = await import("child_process");
1248
+ const openCmd = process.platform === "darwin" ? "open" :
1249
+ process.platform === "win32" ? "start" : "xdg-open";
1250
+ exec(`${openCmd} "${authData.url}"`);
1251
+ } catch {
1252
+ warn("브라우저를 자동으로 열 수 없습니다. 위 URL을 직접 열어주세요.");
1253
+ }
1254
+
1255
+ // 3. 폴링 — 2초마다 인증 완료 확인
1256
+ info("로그인 대기 중...");
1257
+ const POLL_INTERVAL = 2000;
1258
+ const MAX_POLLS = 150; // 10분 내 5분 = 150회
1259
+
1260
+ for (let i = 0; i < MAX_POLLS; i++) {
1261
+ await new Promise(r => setTimeout(r, POLL_INTERVAL));
1262
+
1263
+ try {
1264
+ const res = await fetch(`${platformUrl}/platform/cli/auth-poll`, {
1265
+ method: "POST",
1266
+ headers: { "Content-Type": "application/json" },
1267
+ body: JSON.stringify({ code: authData.code }),
1268
+ });
1269
+ const data = await res.json();
1270
+
1271
+ if (data.status === "completed") {
1272
+ // 4. 토큰 저장
1273
+ saveCreds({
1274
+ apiKey: data.token,
1275
+ userId: data.userId,
1276
+ platformUrl,
1277
+ loginAt: new Date().toISOString(),
1278
+ });
1279
+
1280
+ log("");
1281
+ success(`로그인 완료!`);
1282
+ info(`토큰 저장됨: ${DIM}~/.gencow/credentials.json${RESET}`);
1283
+ log("");
1284
+ return;
1285
+ }
1286
+
1287
+ if (data.status === "expired") {
1288
+ error("인증 코드가 만료되었습니다. 다시 시도하세요: gencow login");
1289
+ process.exit(1);
1290
+ }
1291
+
1292
+ // pending — 계속 폴링
1293
+ process.stdout.write(".");
1294
+ } catch {
1295
+ // 네트워크 오류 — 재시도
1296
+ process.stdout.write("x");
1297
+ }
1298
+ }
1299
+
1300
+ error("\n타임아웃. 다시 시도하세요: gencow login");
1301
+ process.exit(1);
1302
+ },
1303
+
1304
+ // ── logout ────────────────────────────────────────────
1305
+ async logout() {
1306
+ try {
1307
+ unlinkSync(CREDS_PATH);
1308
+ success("로그아웃 완료. 자격 증명이 삭제되었습니다.");
1309
+ } catch {
1310
+ info("이미 로그아웃 상태입니다.");
1311
+ }
1312
+ },
1313
+
1314
+ // ── whoami ────────────────────────────────────────────
1315
+ async whoami() {
1316
+ const creds = loadCreds();
1317
+ if (!creds?.apiKey) {
1318
+ error("로그인하지 않았습니다.");
1319
+ info("실행: gencow login");
1320
+ return;
1321
+ }
1322
+
1323
+ try {
1324
+ const res = await fetch(`${creds.platformUrl}/platform/me`, {
1325
+ headers: { "Authorization": `Bearer ${creds.apiKey}` },
1326
+ });
1327
+
1328
+ if (!res.ok) {
1329
+ error("토큰이 만료되었거나 유효하지 않습니다.");
1330
+ info("실행: gencow login");
1331
+ return;
1332
+ }
1333
+
1334
+ const data = await res.json();
1335
+ log(`\n${BOLD}${CYAN}Gencow CLI${RESET}\n`);
1336
+ info(`User ID: ${data.userId}`);
1337
+ info(`Platform: ${creds.platformUrl}`);
1338
+ info(`Token: ${creds.apiKey.slice(0, 10)}...${creds.apiKey.slice(-4)}`);
1339
+ info(`Expires: ${new Date(data.expiresAt).toLocaleString()}`);
1340
+ log("");
1341
+ } catch (e) {
1342
+ error(`서버 연결 실패: ${e.message}`);
1343
+ }
1344
+ },
1345
+
1346
+ // ── deploy ────────────────────────────────────────────
1347
+ async deploy(...deployArgs) {
1348
+ const creds = requireCreds();
1349
+
1350
+ // gencow.json 또는 gencow.config.ts에서 앱 이름 확인
1351
+ let appName = null;
1352
+ let envTarget = "dev";
1353
+
1354
+ for (let i = 0; i < deployArgs.length; i++) {
1355
+ const a = deployArgs[i];
1356
+ if (a === "--prod") envTarget = "prod";
1357
+ else if (a === "--name" || a === "-n") appName = deployArgs[++i];
1358
+ else if (!a.startsWith("-")) appName = a;
1359
+ }
1360
+
1361
+ // gencow.json에서 앱 이름 로드
1362
+ const gencowJsonPath = resolve(process.cwd(), "gencow.json");
1363
+ if (!appName && existsSync(gencowJsonPath)) {
1364
+ const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
1365
+ appName = gencowJson.appName;
1366
+ }
1367
+
1368
+ if (!appName) {
1369
+ // gencow.config.ts에서 프로젝트 이름 추출 시도
1370
+ const configPath = resolve(process.cwd(), "gencow.config.ts");
1371
+ if (existsSync(configPath)) {
1372
+ const content = readFileSync(configPath, "utf8");
1373
+ const match = content.match(/name:\s*["']([^"']+)["']/);
1374
+ if (match) appName = match[1];
1375
+ }
1376
+ }
1377
+
1378
+ if (!appName) {
1379
+ // 디렉토리 이름을 앱 이름으로 사용
1380
+ appName = basename(process.cwd());
1381
+ }
1382
+
1383
+ log(`\n${BOLD}${CYAN}Gencow Deploy${RESET}\n`);
1384
+ info(`앱: ${appName}`);
1385
+ info(`환경: ${envTarget}`);
1386
+ info(`포맷: tar.gz`);
1387
+ log("");
1388
+
1389
+ // 프로덕션 배포 확인
1390
+ if (envTarget === "prod") {
1391
+ const { createInterface } = await import("readline");
1392
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1393
+ const answer = await new Promise(resolve => {
1394
+ rl.question(` ${YELLOW}⚠${RESET} 프로덕션 배포를 진행하시겠습니까? (y/N) `, resolve);
1395
+ });
1396
+ rl.close();
1397
+ if (answer.toLowerCase() !== "y") {
1398
+ info("배포 취소됨.");
1399
+ return;
1400
+ }
1401
+ }
1402
+
1403
+ // 1. tar.gz 패키징
1404
+ info("프로젝트 패키징 중...");
1405
+ const { execSync: exec } = await import("child_process");
1406
+ const tmpBundle = resolve(process.cwd(), ".gencow", "deploy-bundle.tar.gz");
1407
+ mkdirSync(dirname(tmpBundle), { recursive: true });
1408
+
1409
+ // gencow/ 폴더 + package.json + .env (있으면) 패키징
1410
+ const filesToPack = ["gencow/", "package.json"];
1411
+ if (existsSync(resolve(process.cwd(), "bun.lockb"))) filesToPack.push("bun.lockb");
1412
+ if (existsSync(resolve(process.cwd(), "package-lock.json"))) filesToPack.push("package-lock.json");
1413
+ if (existsSync(resolve(process.cwd(), "tsconfig.json"))) filesToPack.push("tsconfig.json");
1414
+
1415
+ // gencow/ 디렉토리 존재 확인
1416
+ if (!existsSync(resolve(process.cwd(), "gencow"))) {
1417
+ error("gencow/ 디렉토리가 없습니다. Gencow 프로젝트 루트에서 실행하세요.");
1418
+ process.exit(1);
1419
+ }
1420
+
1421
+ try {
1422
+ exec(`tar -czf "${tmpBundle}" ${filesToPack.join(" ")}`, { cwd: process.cwd() });
1423
+ } catch (e) {
1424
+ error(`패키징 실패: ${e.message}`);
1425
+ process.exit(1);
1426
+ }
1427
+
1428
+ const bundleSize = statSync(tmpBundle).size;
1429
+ success(`번들 생성: ${(bundleSize / 1024).toFixed(1)} KB`);
1430
+
1431
+ // 2. 앱 생성 or 확인
1432
+ info("앱 상태 확인 중...");
1433
+ const checkRes = await platformFetch(creds, `/platform/apps/${appName}/deploy`, { method: "HEAD" }).catch(() => null);
1434
+
1435
+ // 앱이 없으면 먼저 생성 (RPC mutation 호출)
1436
+ // Note: deploy API는 앱이 이미 존재해야 함
1437
+ // 여기서는 일단 deploy 시도 — 404면 앱 생성 후 재시도
1438
+
1439
+ // 3. 업로드
1440
+ info("배포 중...");
1441
+ const bundleBuffer = readFileSync(tmpBundle);
1442
+
1443
+ const deployRes = await fetch(
1444
+ `${creds.platformUrl}/platform/apps/${appName}/deploy?env=${envTarget}`,
1445
+ {
1446
+ method: "POST",
1447
+ headers: {
1448
+ "Authorization": `Bearer ${creds.apiKey}`,
1449
+ "Content-Type": "application/octet-stream",
1450
+ },
1451
+ body: bundleBuffer,
1452
+ }
1453
+ );
1454
+
1455
+ // 임시 파일 정리
1456
+ try { unlinkSync(tmpBundle); } catch { }
1457
+
1458
+ if (!deployRes.ok) {
1459
+ const errData = await deployRes.json().catch(() => ({}));
1460
+
1461
+ // 앱이 없으면 생성 후 재시도
1462
+ if (deployRes.status === 404) {
1463
+ warn(`앱 "${appName}"이 아직 없습니다. 생성합니다...`);
1464
+
1465
+ const createRes = await fetch(`${creds.platformUrl}/api/mutation`, {
1466
+ method: "POST",
1467
+ headers: {
1468
+ "Content-Type": "application/json",
1469
+ "Authorization": `Bearer ${creds.apiKey}`,
1470
+ },
1471
+ body: JSON.stringify({ name: "apps.create", args: { name: appName } }),
1472
+ });
1473
+
1474
+ if (!createRes.ok) {
1475
+ const createErr = await createRes.json().catch(() => ({}));
1476
+ error(`앱 생성 실패: ${createErr.error || createRes.statusText}`);
1477
+ process.exit(1);
1478
+ }
1479
+
1480
+ success(`앱 "${appName}" 생성 완료. 프로비저닝 대기 중...`);
1481
+ await new Promise(r => setTimeout(r, 3000));
1482
+
1483
+ // 재배포
1484
+ info("재배포 시도...");
1485
+ // tar.gz를 다시 만들어야 함 (위에서 삭제했으므로)
1486
+ exec(`tar -czf "${tmpBundle}" ${filesToPack.join(" ")}`, { cwd: process.cwd() });
1487
+ const retryBuffer = readFileSync(tmpBundle);
1488
+ const retryRes = await fetch(
1489
+ `${creds.platformUrl}/platform/apps/${appName}/deploy?env=${envTarget}`,
1490
+ {
1491
+ method: "POST",
1492
+ headers: {
1493
+ "Authorization": `Bearer ${creds.apiKey}`,
1494
+ "Content-Type": "application/octet-stream",
1495
+ },
1496
+ body: retryBuffer,
1497
+ }
1498
+ );
1499
+ try { unlinkSync(tmpBundle); } catch { }
1500
+
1501
+ if (!retryRes.ok) {
1502
+ const retryErr = await retryRes.json().catch(() => ({}));
1503
+ error(`배포 실패: ${retryErr.error || retryRes.statusText}`);
1504
+ process.exit(1);
1505
+ }
1506
+
1507
+ const retryData = await retryRes.json();
1508
+ log("");
1509
+ success(`배포 완료!`);
1510
+ info(`URL: ${retryData.url}`);
1511
+ info(`Hash: ${retryData.bundleHash}`);
1512
+ log("");
1513
+ return;
1514
+ }
1515
+
1516
+ error(`배포 실패: ${errData.error || deployRes.statusText}`);
1517
+ process.exit(1);
1518
+ }
1519
+
1520
+ const deployData = await deployRes.json();
1521
+
1522
+ log("");
1523
+ success(`배포 완료!`);
1524
+ info(`URL: ${deployData.url}`);
1525
+ info(`Hash: ${deployData.bundleHash}`);
1526
+ if (deployData.deployId) info(`ID: ${deployData.deployId}`);
1527
+ log("");
1528
+
1529
+ // gencow.json 업데이트 (앱 이름 저장)
1530
+ if (!existsSync(gencowJsonPath)) {
1531
+ writeFileSync(gencowJsonPath, JSON.stringify({
1532
+ appName,
1533
+ platformUrl: creds.platformUrl,
1534
+ }, null, 2));
1535
+ info(`${DIM}gencow.json 생성됨${RESET}`);
1536
+ }
1537
+ },
1538
+
1539
+ // ── env ───────────────────────────────────────────────
1540
+ async env(...envArgs) {
1541
+ const creds = requireCreds();
1542
+ const subCmd = envArgs[0] || "list";
1543
+ const restArgs = envArgs.slice(1);
1544
+
1545
+ // 앱 이름 결정
1546
+ let appName = null;
1547
+ let envTarget = "dev";
1548
+
1549
+ for (let i = 0; i < restArgs.length; i++) {
1550
+ if (restArgs[i] === "--app" || restArgs[i] === "-a") appName = restArgs[++i];
1551
+ if (restArgs[i] === "--prod") envTarget = "prod";
1552
+ }
1553
+
1554
+ if (!appName) {
1555
+ const gencowJsonPath = resolve(process.cwd(), "gencow.json");
1556
+ if (existsSync(gencowJsonPath)) {
1557
+ const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
1558
+ appName = gencowJson.appName;
1559
+ }
1560
+ }
1561
+
1562
+ if (!appName) {
1563
+ appName = basename(process.cwd());
1564
+ }
1565
+
1566
+ switch (subCmd) {
1567
+ case "list":
1568
+ case "ls": {
1569
+ const res = await fetch(
1570
+ `${creds.platformUrl}/platform/apps/${appName}/env?env=${envTarget}`,
1571
+ { headers: { "Authorization": `Bearer ${creds.apiKey}` } }
1572
+ );
1573
+ if (!res.ok) {
1574
+ error(`환경변수 조회 실패: ${(await res.json().catch(() => ({}))).error || res.statusText}`);
1575
+ return;
1576
+ }
1577
+ const vars = await res.json();
1578
+ log(`\n${BOLD}${CYAN}환경변수${RESET} — ${appName} (${envTarget})\n`);
1579
+ if (vars.length === 0) {
1580
+ info("설정된 환경변수가 없습니다.");
1581
+ } else {
1582
+ for (const v of vars) {
1583
+ log(` ${v.key}`);
1584
+ }
1585
+ }
1586
+ log("");
1587
+ break;
1588
+ }
1589
+
1590
+ case "set": {
1591
+ // gencow env set KEY=VALUE [KEY2=VALUE2...]
1592
+ const kvPairs = restArgs.filter(a => a.includes("=") && !a.startsWith("-"));
1593
+ if (kvPairs.length === 0) {
1594
+ error("사용법: gencow env set KEY=VALUE [KEY2=VALUE2...]");
1595
+ return;
1596
+ }
1597
+
1598
+ for (const kv of kvPairs) {
1599
+ const [key, ...valueParts] = kv.split("=");
1600
+ const value = valueParts.join("=");
1601
+
1602
+ const res = await fetch(
1603
+ `${creds.platformUrl}/platform/apps/${appName}/env`,
1604
+ {
1605
+ method: "POST",
1606
+ headers: {
1607
+ "Authorization": `Bearer ${creds.apiKey}`,
1608
+ "Content-Type": "application/json",
1609
+ },
1610
+ body: JSON.stringify({ key, value, env: envTarget }),
1611
+ }
1612
+ );
1613
+
1614
+ if (res.ok) {
1615
+ success(`${key} 설정 완료 (${envTarget})`);
1616
+ } else {
1617
+ error(`${key} 설정 실패`);
1618
+ }
1619
+ }
1620
+ break;
1621
+ }
1622
+
1623
+ case "unset":
1624
+ case "remove": {
1625
+ const keys = restArgs.filter(a => !a.startsWith("-"));
1626
+ if (keys.length === 0) {
1627
+ error("사용법: gencow env unset KEY [KEY2...]");
1628
+ return;
1629
+ }
1630
+
1631
+ for (const key of keys) {
1632
+ const res = await fetch(
1633
+ `${creds.platformUrl}/platform/apps/${appName}/env/${key}?env=${envTarget}`,
1634
+ {
1635
+ method: "DELETE",
1636
+ headers: { "Authorization": `Bearer ${creds.apiKey}` },
1637
+ }
1638
+ );
1639
+
1640
+ if (res.ok) {
1641
+ success(`${key} 삭제 완료`);
1642
+ } else {
1643
+ error(`${key} 삭제 실패`);
1644
+ }
1645
+ }
1646
+ break;
1647
+ }
1648
+
1649
+ case "push": {
1650
+ // 로컬 .env 파일 → 리모트 일괄 push
1651
+ const envFile = resolve(process.cwd(), envTarget === "prod" ? ".env.production" : ".env");
1652
+ if (!existsSync(envFile)) {
1653
+ error(`${envFile} 파일을 찾을 수 없습니다.`);
1654
+ return;
1655
+ }
1656
+
1657
+ const envContent = readFileSync(envFile, "utf8");
1658
+ const vars = {};
1659
+ for (const line of envContent.split("\n")) {
1660
+ const trimmed = line.trim();
1661
+ if (!trimmed || trimmed.startsWith("#")) continue;
1662
+ const [key, ...valueParts] = trimmed.split("=");
1663
+ if (key) vars[key.trim()] = valueParts.join("=").trim();
1664
+ }
1665
+
1666
+ const count = Object.keys(vars).length;
1667
+ info(`${count}개 환경변수를 ${appName} (${envTarget})에 push합니다...`);
1668
+
1669
+ const res = await fetch(
1670
+ `${creds.platformUrl}/platform/apps/${appName}/env/bulk`,
1671
+ {
1672
+ method: "PUT",
1673
+ headers: {
1674
+ "Authorization": `Bearer ${creds.apiKey}`,
1675
+ "Content-Type": "application/json",
1676
+ },
1677
+ body: JSON.stringify({ vars, env: envTarget }),
1678
+ }
1679
+ );
1680
+
1681
+ if (res.ok) {
1682
+ success(`${count}개 환경변수 push 완료`);
1683
+ } else {
1684
+ error(`push 실패: ${(await res.json().catch(() => ({}))).error}`);
1685
+ }
1686
+ break;
1687
+ }
1688
+
1689
+ default:
1690
+ error(`알 수 없는 하위 명령: ${subCmd}`);
1691
+ info("사용법: gencow env [list|set|unset|push] ...");
1692
+ }
1693
+ },
1694
+
1695
+
1216
1696
  async mcp(...mcpArgs) {
1217
1697
  let port = "5456";
1218
1698
  for (let i = 0; i < mcpArgs.length; i++) {
@@ -1667,15 +2147,19 @@ ${BOLD}${CYAN}Gencow Dev Remote${RESET}
1667
2147
  log(`\n${BOLD}${CYAN}gencow add${RESET} — Add components to your project\n`);
1668
2148
  log(`${BOLD}Usage:${RESET} gencow add <component...>\n`);
1669
2149
  log(`${BOLD}Available components:${RESET}`);
1670
- log(` ${GREEN}AI${RESET} Vercel AI SDK wrapper ${DIM}(chat, stream, embed)${RESET}`);
1671
- log(` ${GREEN}Tools${RESET} AI Tool Calling with ctx integration`);
1672
- log(` ${GREEN}RAG${RESET} Document ingestion + semantic search`);
1673
- log(` ${GREEN}Memory${RESET} Agent memory ${DIM}(episodic/semantic/procedural)${RESET}`);
1674
- log(` ${GREEN}Analytics${RESET} LLM call tracking ${DIM}(tokens, cost, latency)${RESET}`);
2150
+ log(` ${GREEN}AI${RESET} Vercel AI SDK wrapper ${DIM}(chat, stream, embed, agent, retry)${RESET}`);
2151
+ log(` ${GREEN}Tools${RESET} AI Tool Calling with ctx integration`);
2152
+ log(` ${GREEN}RAG${RESET} Document ingestion + semantic search + Q&A`);
2153
+ log(` ${GREEN}Reranker${RESET} LLM-powered search result reranking`);
2154
+ log(` ${GREEN}Guardrails${RESET} Input/output safety filters`);
2155
+ log(` ${GREEN}Prompts${RESET} Reusable prompt templates`);
2156
+ log(` ${GREEN}Parsers${RESET} PDF/HTML/CSV file parsing`);
2157
+ log(` ${GREEN}Memory${RESET} Agent memory ${DIM}(episodic/semantic/procedural)${RESET}`);
2158
+ log(` ${GREEN}Analytics${RESET} LLM call tracking ${DIM}(coming soon)${RESET}`);
1675
2159
  log(`\n${BOLD}Examples:${RESET}`);
1676
2160
  log(` ${DIM}gencow add AI${RESET}`);
1677
- log(` ${DIM}gencow add AI RAG Tools${RESET}`);
1678
- log(` ${DIM}gencow add Memory${RESET}\n`);
2161
+ log(` ${DIM}gencow add AI RAG Reranker${RESET}`);
2162
+ log(` ${DIM}gencow add Guardrails Prompts${RESET}\n`);
1679
2163
  return;
1680
2164
  }
1681
2165
 
@@ -1750,6 +2234,63 @@ ${BOLD}${CYAN}Gencow Dev Remote${RESET}
1750
2234
  notReady: true,
1751
2235
  guide: [],
1752
2236
  },
2237
+ reranker: {
2238
+ label: "Reranker",
2239
+ deps: [],
2240
+ files: [
2241
+ { src: "reranker.ts", dest: "gencow/reranker.ts" },
2242
+ ],
2243
+ env: {},
2244
+ requires: ["ai"],
2245
+ guide: [
2246
+ `gencow/reranker.ts 에서 reranker.rerank(), reranker.searchAndRerank() 사용 가능`,
2247
+ `const reranked = await reranker.rerank(query, searchResults, { topK: 5 })`,
2248
+ `RAG 파이프라인: await reranker.searchAndRerank(ctx, rag, query)`,
2249
+ ],
2250
+ },
2251
+ guardrails: {
2252
+ label: "Guardrails",
2253
+ deps: [],
2254
+ files: [
2255
+ { src: "guardrails.ts", dest: "gencow/guardrails.ts" },
2256
+ ],
2257
+ env: {},
2258
+ requires: ["ai"],
2259
+ guide: [
2260
+ `gencow/guardrails.ts 에서 guardrails.validateInput(), guardrails.validateOutput() 사용 가능`,
2261
+ `PII 마스킹: guardrails.validateInput(text, { maskPII: true })`,
2262
+ `주제 차단: guardrails.validateInput(text, { blockTopics: ["정치"] })`,
2263
+ `한 번에 래핑: guardrails.wrap(fn, input, inputOpts, outputOpts)`,
2264
+ ],
2265
+ },
2266
+ prompts: {
2267
+ label: "Prompt Templates",
2268
+ deps: [],
2269
+ files: [
2270
+ { src: "prompts.ts", dest: "gencow/prompts.ts" },
2271
+ ],
2272
+ env: {},
2273
+ requires: [],
2274
+ guide: [
2275
+ `gencow/prompts.ts 에서 definePrompt() 로 재사용 프롬프트 정의`,
2276
+ `내장 템플릿: ragQAPrompt, summarizePrompt, classifyPrompt`,
2277
+ `const filled = ragQAPrompt({ question: "...", context: "..." })`,
2278
+ ],
2279
+ },
2280
+ parsers: {
2281
+ label: "File Parsers",
2282
+ deps: ["pdf-parse"],
2283
+ files: [
2284
+ { src: "parsers.ts", dest: "gencow/parsers.ts" },
2285
+ ],
2286
+ env: {},
2287
+ requires: [],
2288
+ guide: [
2289
+ `gencow/parsers.ts 에서 parsers.pdf(), parsers.html(), parsers.csv() 사용 가능`,
2290
+ `자동 감지: await parsers.auto("file.pdf", buffer)`,
2291
+ `RAG 연동: await rag.ingest(ctx, "file.pdf", await parsers.pdf(buffer))`,
2292
+ ],
2293
+ },
1753
2294
  };
1754
2295
 
1755
2296
  log(`\n${BOLD}${CYAN}gencow add${RESET}\n`);
@@ -1897,7 +2438,13 @@ const COMPONENT_DOCS = {
1897
2438
  | :--- | :--- | :--- |
1898
2439
  | \`ai.chat()\` | \`function\` | 텍스트 생성 (비스트리밍) |
1899
2440
  | \`ai.stream()\` | \`function\` | 스트리밍 응답 |
1900
- | \`ai.embed()\` | \`function\` | 텍스트 임베딩 (RAG/Memory용) |`,
2441
+ | \`ai.embed()\` | \`function\` | 텍스트 임베딩 (RAG/Memory용) |
2442
+ | \`ai.embedMany()\` | \`function\` | 배치 임베딩 (대용량 인제스트) |
2443
+ | \`ai.generateObject()\` | \`function\` | 구조화 JSON 출력 (Zod 스키마) |
2444
+ | \`ai.agent()\` | \`function\` | Agent Loop (multi-turn tool use) |
2445
+ | \`ai.withRetry()\` | \`function\` | 자동 재시도 + 폴백 미들웨어 |
2446
+ | \`ai.estimateTokens()\` | \`function\` | 토큰 수 추정 (간이) |
2447
+ | \`ai.trimMessages()\` | \`function\` | 메시지 토큰 예산 자르기 |`,
1901
2448
  usage: `\`\`\`typescript
1902
2449
  import { ai } from "@/gencow/ai";
1903
2450
 
@@ -1907,37 +2454,62 @@ const reply = await ai.chat({
1907
2454
  messages: [{ role: "user", content: "안녕?" }],
1908
2455
  });
1909
2456
 
1910
- // 스트리밍
1911
- const stream = await ai.stream({ messages: [...] });
1912
- for await (const chunk of stream.textStream) {
1913
- process.stdout.write(chunk);
1914
- }
2457
+ // Agent Loop — 도구 자동 반복 호출
2458
+ const result = await ai.agent({
2459
+ system: "고객 지원 에이전트입니다.",
2460
+ messages: [{ role: "user", content: "주문 ORD-123 환불해줘" }],
2461
+ tools: defineTools(ctx, { ... }),
2462
+ maxSteps: 5,
2463
+ });
2464
+
2465
+ // 재시도 + 폴백
2466
+ const safe = await ai.withRetry(
2467
+ () => ai.chat({ messages: [...] }),
2468
+ { maxRetries: 3 }
2469
+ );
1915
2470
  \`\`\``,
1916
2471
  vibePrompt: `AI 엔진 사용법:
1917
2472
  import { ai } from "@/gencow/ai";
1918
2473
  ai.chat({ messages }) // 텍스트 생성
1919
2474
  ai.stream({ messages }) // 스트리밍
1920
- ai.embed("텍스트") // 임베딩`,
2475
+ ai.embed("텍스트") // 임베딩
2476
+ ai.embedMany(["a","b"]) // 배치 임베딩
2477
+ ai.agent({ messages, tools, maxSteps }) // Agent Loop
2478
+ ai.withRetry(fn, { maxRetries: 3 }) // 재시도+폴백
2479
+ ai.estimateTokens(text) // 토큰 추정
2480
+ ai.trimMessages(msgs, { maxTokens: 4000 }) // 토큰 예산`,
1921
2481
  },
1922
2482
  "rag.ts": {
1923
2483
  title: "🔍 RAG Engine",
1924
2484
  table: `| Function | Type | Description |
1925
2485
  | :--- | :--- | :--- |
1926
- | \`rag.ingest()\` | \`function\` | 문서 청킹 + 임베딩 저장 |
1927
- | \`rag.search()\` | \`function\` | 시맨틱 검색 (코사인 유사도) |`,
2486
+ | \`rag.ingest()\` | \`function\` | 문서 청킹 + 임베딩 저장 (스마트 청킹, 배치 임베딩) |
2487
+ | \`rag.search()\` | \`function\` | 시맨틱 검색 (코사인 유사도, 소스 필터) |
2488
+ | \`rag.ask()\` | \`function\` | RAG Q&A (검색→컨텍스트→AI 답변) |
2489
+ | \`rag.delete()\` | \`function\` | 소스별 문서 삭제 |`,
1928
2490
  usage: `\`\`\`typescript
1929
2491
  import { rag } from "@/gencow/rag";
1930
2492
 
1931
- // mutation 안에서 문서 인제스트
1932
- await rag.ingest(ctx, "manual.pdf", documentText);
2493
+ // mutation 안에서 문서 인제스트 (스마트 청킹 자동 적용)
2494
+ await rag.ingest(ctx, "manual.pdf", documentText, { strategy: "markdown" });
1933
2495
 
1934
- // query 안에서 시맨틱 검색
1935
- const results = await rag.search(ctx, "환불 정책이 뭔가요?");
2496
+ // query 안에서 시맨틱 검색 (소스 필터 가능)
2497
+ const results = await rag.search(ctx, "환불 정책이 뭔가요?", {
2498
+ filter: { source: "manual.pdf" },
2499
+ });
2500
+
2501
+ // Q&A — 검색+답변 한 번에
2502
+ const { answer, sources } = await rag.ask(ctx, "환불 정책이 뭔가요?");
2503
+
2504
+ // 문서 삭제
2505
+ await rag.delete(ctx, "old-manual.pdf");
1936
2506
  \`\`\``,
1937
2507
  vibePrompt: `RAG 엔진 사용법:
1938
2508
  import { rag } from "@/gencow/rag";
1939
- rag.ingest(ctx, source, text) // 문서 인제스트
1940
- rag.search(ctx, query) // 시맨틱 검색`,
2509
+ rag.ingest(ctx, source, text) // 문서 인제스트 (스마트 청킹)
2510
+ rag.search(ctx, query, { filter }) // 시맨틱 검색
2511
+ rag.ask(ctx, question) // Q&A (검색+답변)
2512
+ rag.delete(ctx, source) // 문서 삭제`,
1941
2513
  },
1942
2514
  "tools.ts": {
1943
2515
  title: "🔧 Tool Calling",
@@ -1998,6 +2570,110 @@ const reply = await ai.chat({
1998
2570
  vibePrompt: `Analytics:
1999
2571
  ai.chat()/ai.stream() 호출 시 자동 계측됩니다.`,
2000
2572
  },
2573
+ "reranker.ts": {
2574
+ title: "🎯 Reranker",
2575
+ table: `| Function | Type | Description |
2576
+ | :--- | :--- | :--- |
2577
+ | \`reranker.rerank()\` | \`function\` | LLM 기반 검색 결과 재정렬 |
2578
+ | \`reranker.searchAndRerank()\` | \`function\` | RAG search + rerank 통합 |`,
2579
+ usage: `\`\`\`typescript
2580
+ import { reranker } from "@/gencow/reranker";
2581
+ import { rag } from "@/gencow/rag";
2582
+
2583
+ // 벡터 검색 후 LLM 재정렬
2584
+ const results = await rag.search(ctx, query, { limit: 20 });
2585
+ const reranked = await reranker.rerank(query, results, { topK: 5 });
2586
+
2587
+ // 한 번에 (search + rerank)
2588
+ const best = await reranker.searchAndRerank(ctx, rag, query);
2589
+ \`\`\``,
2590
+ vibePrompt: `Reranker 사용법:
2591
+ import { reranker } from "@/gencow/reranker";
2592
+ reranker.rerank(query, docs, { topK: 5 }) // LLM 재정렬
2593
+ reranker.searchAndRerank(ctx, rag, query) // 통합 파이프라인`,
2594
+ },
2595
+ "guardrails.ts": {
2596
+ title: "🛡️ Guardrails",
2597
+ table: `| Function | Type | Description |
2598
+ | :--- | :--- | :--- |
2599
+ | \`guardrails.validateInput()\` | \`function\` | 입력 검증 (PII/주제/길이) |
2600
+ | \`guardrails.validateOutput()\` | \`function\` | 출력 검증 (길이/패턴/커스텀) |
2601
+ | \`guardrails.wrap()\` | \`function\` | 입력→AI→출력 한 번에 래핑 |`,
2602
+ usage: `\`\`\`typescript
2603
+ import { guardrails } from "@/gencow/guardrails";
2604
+
2605
+ // PII 마스킹 + 주제 차단
2606
+ const safe = await guardrails.validateInput(userMsg, {
2607
+ maskPII: true,
2608
+ blockTopics: ["정치", "종교"],
2609
+ });
2610
+ if (!safe.allowed) return safe.blocked;
2611
+
2612
+ // 래핑 유틸리티
2613
+ const result = await guardrails.wrap(
2614
+ (sanitized) => ai.chat({ messages: [{ role: "user", content: sanitized }] }),
2615
+ userMsg,
2616
+ { maskPII: true },
2617
+ { maxLength: 2000 }
2618
+ );
2619
+ \`\`\``,
2620
+ vibePrompt: `Guardrails 사용법:
2621
+ import { guardrails } from "@/gencow/guardrails";
2622
+ guardrails.validateInput(text, { maskPII: true }) // PII 마스킹
2623
+ guardrails.validateInput(text, { blockTopics }) // 주제 차단
2624
+ guardrails.wrap(fn, input, inputOpts, outputOpts) // 한 번에 래핑`,
2625
+ },
2626
+ "prompts.ts": {
2627
+ title: "📝 Prompt Templates",
2628
+ table: `| Function | Type | Description |
2629
+ | :--- | :--- | :--- |
2630
+ | \`definePrompt()\` | \`function\` | 재사용 프롬프트 팩토리 |
2631
+ | \`ragQAPrompt\` | \`template\` | RAG Q&A 내장 프롬프트 |
2632
+ | \`summarizePrompt\` | \`template\` | 요약 내장 프롬프트 |
2633
+ | \`classifyPrompt\` | \`template\` | 분류 내장 프롬프트 |`,
2634
+ usage: `\`\`\`typescript
2635
+ import { definePrompt, ragQAPrompt } from "@/gencow/prompts";
2636
+
2637
+ // 내장 템플릿 사용
2638
+ const filled = ragQAPrompt({ question: "환불 정책?", context: docs });
2639
+
2640
+ // 커스텀 프롬프트 정의
2641
+ const myPrompt = definePrompt({
2642
+ template: "{{role}}로서 {{task}}를 수행하세요.",
2643
+ defaults: { role: "도움이 되는 AI" },
2644
+ });
2645
+ const prompt = myPrompt({ role: "전문가", task: "코드 리뷰" });
2646
+ \`\`\``,
2647
+ vibePrompt: `Prompt Templates 사용법:
2648
+ import { definePrompt, ragQAPrompt } from "@/gencow/prompts";
2649
+ definePrompt({ template, defaults }) // 재사용 프롬프트 정의
2650
+ ragQAPrompt({ question, context }) // RAG Q&A 프롬프트`,
2651
+ },
2652
+ "parsers.ts": {
2653
+ title: "📄 File Parsers",
2654
+ table: `| Function | Type | Description |
2655
+ | :--- | :--- | :--- |
2656
+ | \`parsers.pdf()\` | \`function\` | PDF → 텍스트 추출 |
2657
+ | \`parsers.html()\` | \`function\` | HTML → 텍스트 (태그 제거) |
2658
+ | \`parsers.csv()\` | \`function\` | CSV → 행별 텍스트 |
2659
+ | \`parsers.auto()\` | \`function\` | 확장자 기반 자동 파싱 |`,
2660
+ usage: `\`\`\`typescript
2661
+ import { parsers } from "@/gencow/parsers";
2662
+
2663
+ // PDF 파싱 후 RAG 인제스트
2664
+ const text = await parsers.pdf(fileBuffer);
2665
+ await rag.ingest(ctx, "manual.pdf", text);
2666
+
2667
+ // 자동 감지
2668
+ const content = await parsers.auto("report.html", htmlString);
2669
+ \`\`\``,
2670
+ vibePrompt: `File Parsers 사용법:
2671
+ import { parsers } from "@/gencow/parsers";
2672
+ parsers.pdf(buffer) // PDF → 텍스트
2673
+ parsers.html(htmlString) // HTML → 텍스트
2674
+ parsers.csv(csvText) // CSV → ["header: value", ...]
2675
+ parsers.auto(filename, buf) // 확장자 자동 감지`,
2676
+ },
2001
2677
  };
2002
2678
 
2003
2679
  async function updateReadme(config) {