connectbase-client 0.10.3 → 0.10.5

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/cli.js CHANGED
@@ -350,6 +350,84 @@ function getProjectRoot() {
350
350
  const gitRoot = getGitRoot();
351
351
  return gitRoot || process.cwd();
352
352
  }
353
+ var CONSOLE_URL = DEFAULT_BASE_URL.replace("api.", "");
354
+ function openBrowser(url) {
355
+ const { exec } = require("child_process");
356
+ const platform = process.platform;
357
+ let command;
358
+ if (platform === "darwin") {
359
+ command = `open "${url}"`;
360
+ } else if (platform === "win32") {
361
+ command = `start "" "${url}"`;
362
+ } else {
363
+ command = `xdg-open "${url}"`;
364
+ }
365
+ exec(command, () => {
366
+ });
367
+ }
368
+ async function browserAuthFlow() {
369
+ info("\uBE0C\uB77C\uC6B0\uC800 \uC778\uC99D \uC138\uC158\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4...");
370
+ const startRes = await makeRequest(
371
+ `${DEFAULT_BASE_URL}/v1/public/cli-auth/start`,
372
+ "POST",
373
+ {}
374
+ );
375
+ if (startRes.status !== 200) {
376
+ error("\uC778\uC99D \uC138\uC158 \uC2DC\uC791\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4");
377
+ process.exit(1);
378
+ }
379
+ const { session_id } = startRes.data;
380
+ const authUrl = `${CONSOLE_URL}/auth/cli-auth?session=${session_id}`;
381
+ log(`
382
+ ${colors.cyan}\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uB85C\uADF8\uC778\uD558\uC138\uC694:${colors.reset}`);
383
+ log(`${colors.dim}${authUrl}${colors.reset}
384
+ `);
385
+ openBrowser(authUrl);
386
+ const pollInterval = 2e3;
387
+ const maxAttempts = 150;
388
+ let attempts = 0;
389
+ const spinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
390
+ while (attempts < maxAttempts) {
391
+ const frame = spinnerFrames[attempts % spinnerFrames.length];
392
+ process.stdout.write(`\r${colors.blue}${frame}${colors.reset} \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC2B9\uC778 \uB300\uAE30 \uC911...`);
393
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
394
+ try {
395
+ const pollRes = await makeRequest(
396
+ `${DEFAULT_BASE_URL}/v1/public/cli-auth/poll/${session_id}`,
397
+ "GET",
398
+ {}
399
+ );
400
+ if (pollRes.status === 200) {
401
+ const data = pollRes.data;
402
+ if (data.status === "approved" && data.secret_key) {
403
+ process.stdout.write("\r \r");
404
+ success("\uBE0C\uB77C\uC6B0\uC800 \uC778\uC99D\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4!");
405
+ return data.secret_key;
406
+ }
407
+ if (data.status === "expired") {
408
+ process.stdout.write("\r \r");
409
+ error("\uC778\uC99D \uC138\uC158\uC774 \uB9CC\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.");
410
+ process.exit(1);
411
+ }
412
+ if (data.status === "denied") {
413
+ process.stdout.write("\r \r");
414
+ error("\uC778\uC99D\uC774 \uAC70\uBD80\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
415
+ process.exit(1);
416
+ }
417
+ }
418
+ if (pollRes.status === 404) {
419
+ process.stdout.write("\r \r");
420
+ error("\uC778\uC99D \uC138\uC158\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.");
421
+ process.exit(1);
422
+ }
423
+ } catch {
424
+ }
425
+ attempts++;
426
+ }
427
+ process.stdout.write("\r \r");
428
+ error("\uC778\uC99D \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.");
429
+ process.exit(1);
430
+ }
353
431
  async function init() {
354
432
  log(`
355
433
  ${colors.cyan}Connect Base \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654${colors.reset}
@@ -369,21 +447,32 @@ ${colors.cyan}Connect Base \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654${colors.r
369
447
  return;
370
448
  }
371
449
  }
372
- log(`${colors.dim}Secret Key (cb_sk_): \uCF58\uC194 > \uD504\uB85C\uD544 > MCP Key \uD0ED\uC5D0\uC11C \uBC1C\uAE09${colors.reset}
450
+ log(`
451
+ ${colors.dim}\uC778\uC99D \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:${colors.reset}`);
452
+ log(` ${colors.cyan}1${colors.reset}) Secret Key \uC9C1\uC811 \uC785\uB825 (cb_sk_...)`);
453
+ log(` ${colors.cyan}2${colors.reset}) \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uC73C\uB85C \uC790\uB3D9 \uBC1C\uAE09
373
454
  `);
374
- const secretKey = await promptSecret(`${colors.blue}?${colors.reset} Secret Key: `);
375
- if (!secretKey) {
376
- error("Secret Key\uB294 \uD544\uC218\uC785\uB2C8\uB2E4");
377
- process.exit(1);
378
- }
379
- if (secretKey.startsWith("cb_pk_")) {
380
- error("Public Key\uAC00 \uC544\uB2CC Secret Key(cb_sk_)\uB97C \uC785\uB825\uD558\uC138\uC694");
381
- info("Secret Key\uB294 \uCF58\uC194 > \uD504\uB85C\uD544 > MCP Key \uD0ED\uC5D0\uC11C \uC0DD\uC131\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4");
382
- process.exit(1);
383
- }
384
- if (!secretKey.startsWith("cb_sk_")) {
385
- error("\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uD0A4 \uD615\uC2DD\uC785\uB2C8\uB2E4. cb_sk_\uB85C \uC2DC\uC791\uD558\uB294 Secret Key\uB97C \uC785\uB825\uD558\uC138\uC694");
386
- process.exit(1);
455
+ const authChoice = await prompt(`${colors.blue}?${colors.reset} \uC120\uD0DD (1/2): `);
456
+ let secretKey = "";
457
+ if (authChoice === "2") {
458
+ secretKey = await browserAuthFlow();
459
+ } else {
460
+ log(`${colors.dim}Secret Key (cb_sk_): \uCF58\uC194 > \uD504\uB85C\uD544 > \uC2DC\uD06C\uB9BF \uD0A4 \uD0ED\uC5D0\uC11C \uBC1C\uAE09${colors.reset}
461
+ `);
462
+ secretKey = await promptSecret(`${colors.blue}?${colors.reset} Secret Key: `);
463
+ if (!secretKey) {
464
+ error("Secret Key\uB294 \uD544\uC218\uC785\uB2C8\uB2E4");
465
+ process.exit(1);
466
+ }
467
+ if (secretKey.startsWith("cb_pk_")) {
468
+ error("Public Key\uAC00 \uC544\uB2CC Secret Key(cb_sk_)\uB97C \uC785\uB825\uD558\uC138\uC694");
469
+ info("Secret Key\uB294 \uCF58\uC194 > \uD504\uB85C\uD544 > \uC2DC\uD06C\uB9BF \uD0A4 \uD0ED\uC5D0\uC11C \uC0DD\uC131\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4");
470
+ process.exit(1);
471
+ }
472
+ if (!secretKey.startsWith("cb_sk_")) {
473
+ error("\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uD0A4 \uD615\uC2DD\uC785\uB2C8\uB2E4. cb_sk_\uB85C \uC2DC\uC791\uD558\uB294 Secret Key\uB97C \uC785\uB825\uD558\uC138\uC694");
474
+ process.exit(1);
475
+ }
387
476
  }
388
477
  let appId = "";
389
478
  let publicKey = "";
@@ -712,21 +801,12 @@ function updateRootClaudeMd(claudeMdPath) {
712
801
  ## ConnectBase SDK
713
802
 
714
803
  \uC774 \uD504\uB85C\uC81D\uD2B8\uB294 **ConnectBase**\uB97C \uBC31\uC5D4\uB4DC\uB85C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.
715
- \uC0AC\uC6A9\uC790\uB294 **\uBE44\uAC1C\uBC1C\uC790**\uC785\uB2C8\uB2E4. \uC26C\uC6B4 \uB9D0\uB85C \uC124\uBA85\uD558\uACE0, \uBC14\uB85C \uC2E4\uD589 \uAC00\uB2A5\uD55C \uC644\uC131\uB41C \uCF54\uB4DC\uB97C \uC81C\uACF5\uD558\uC138\uC694.
716
804
 
717
805
  ### AI \uD544\uB3C5 \u2014 \uCF54\uB529 \uC804 \uBC18\uB4DC\uC2DC \uC218\uD589
718
806
 
719
- 1. \`.claude/docs/project-rules.md\` \uD30C\uC77C\uC744 **Read tool\uB85C \uC77D\uC73C\uC138\uC694** (\uAE30\uB2A5 \uB9E4\uD551\uD45C, \uBCF4\uC548 \uADDC\uCE59 \uD3EC\uD568)
720
- 2. \uC0AC\uC6A9\uC790 \uC694\uCCAD\uC5D0 \uB9DE\uB294 \uAE30\uB2A5\uC744 \uB9E4\uD551\uD45C\uC5D0\uC11C \uCC3E\uC73C\uC138\uC694
721
- 3. \`search_sdk_docs("\uD0A4\uC6CC\uB4DC")\`\uB85C \uAD6C\uD604 \uBC29\uBC95\uC744 \uAC80\uC0C9\uD558\uC138\uC694
722
- 4. \uC644\uC131\uB41C \uCF54\uB4DC\uB97C \uC791\uC131\uD558\uC138\uC694 (\uC2A4\uB2C8\uD3AB X, \uBC14\uB85C \uC2E4\uD589 \uAC00\uB2A5\uD55C \uC804\uCCB4 \uCF54\uB4DC)
723
-
724
- ### \uBE44\uAC1C\uBC1C\uC790 \uC751\uB300 \uC6D0\uCE59
725
-
726
- - \uC804\uBB38 \uC6A9\uC5B4 \uB300\uC2E0 \uC26C\uC6B4 \uB9D0 \uC0AC\uC6A9 ("API" \u2192 "\uAE30\uB2A5", "DB" \u2192 "\uC800\uC7A5\uC18C")
727
- - \uC5D0\uB7EC \uBC1C\uC0DD \uC2DC \uC6D0\uC778\uACFC \uD574\uACB0\uCC45\uC744 \uD55C \uC904\uB85C \uC124\uBA85
728
- - \uCF54\uB4DC \uC791\uC131 \uC804 "\uC774\uB807\uAC8C \uB9CC\uB4E4\uAC8C\uC694~" \uD55C \uC904 \uC694\uC57D \uBA3C\uC800
729
- - \uBAA8\uB974\uB294 \uAC8C \uC788\uC73C\uBA74 \uB9CC\uB4E4\uAE30 \uC804\uC5D0 \uBA3C\uC800 \uC9C8\uBB38
807
+ 1. \`.claude/docs/project-rules.md\`\uB97C **Read tool\uB85C \uC77D\uC73C\uC138\uC694** (\uAE30\uB2A5 \uB9E4\uD551\uD45C, \uBCF4\uC548 \uADDC\uCE59, \uC5D0\uB7EC \uAC00\uC774\uB4DC)
808
+ 2. \`search_sdk_docs("\uD0A4\uC6CC\uB4DC")\`\uB85C \uAD6C\uD604 \uBC29\uBC95\uC744 \uAC80\uC0C9\uD558\uC138\uC694
809
+ 3. \uAC80\uC0C9 \uACB0\uACFC\uC758 \uCF54\uB4DC \uD328\uD134\uC744 \uB530\uB77C \uAD6C\uD604\uD558\uC138\uC694
730
810
  ${endMarker}`;
731
811
  if (fs.existsSync(claudeMdPath)) {
732
812
  let content = fs.readFileSync(claudeMdPath, "utf-8");
@@ -735,7 +815,7 @@ ${endMarker}`;
735
815
  if (startIdx !== -1 && endIdx !== -1) {
736
816
  content = content.substring(0, startIdx) + sdkBlock + content.substring(endIdx + endMarker.length);
737
817
  } else {
738
- content = content.trimEnd() + "\n\n" + sdkBlock + "\n";
818
+ content = sdkBlock + "\n\n" + content.trimStart();
739
819
  }
740
820
  fs.writeFileSync(claudeMdPath, content);
741
821
  info("CLAUDE.md\uC5D0 ConnectBase \uCC38\uC870 \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC");
@@ -949,15 +1029,89 @@ function getTunnelServerUrl(baseUrl) {
949
1029
  }
950
1030
  return baseUrl.replace(/:\d+/, ":8090");
951
1031
  }
1032
+ async function resolveAppForTunnel(apiKey, baseUrl, appIdOption) {
1033
+ if (appIdOption) {
1034
+ return appIdOption;
1035
+ }
1036
+ let apps = [];
1037
+ try {
1038
+ info("\uC571 \uBAA9\uB85D \uC870\uD68C \uC911...");
1039
+ const appsRes = await makeRequest(
1040
+ `${baseUrl}/v1/public/cli/apps`,
1041
+ "GET",
1042
+ { "X-API-Key": apiKey }
1043
+ );
1044
+ if (appsRes.status === 401) {
1045
+ error("Secret Key\uAC00 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uCF58\uC194\uC5D0\uC11C \uD0A4\uB97C \uD655\uC778\uD558\uC138\uC694");
1046
+ process.exit(1);
1047
+ }
1048
+ if (appsRes.status !== 200) {
1049
+ throw new Error(`HTTP ${appsRes.status}`);
1050
+ }
1051
+ const appsData = appsRes.data;
1052
+ apps = appsData.apps || [];
1053
+ } catch (err) {
1054
+ error(`\uC571 \uBAA9\uB85D \uC870\uD68C \uC2E4\uD328: ${err instanceof Error ? err.message : err}`);
1055
+ process.exit(1);
1056
+ }
1057
+ if (apps.length === 1) {
1058
+ success(`\uC571 \uC790\uB3D9 \uC120\uD0DD: ${apps[0].name}`);
1059
+ return apps[0].id;
1060
+ }
1061
+ if (apps.length > 0) {
1062
+ log(`
1063
+ ${colors.dim}\uB0B4 \uC571 \uBAA9\uB85D:${colors.reset}`);
1064
+ apps.forEach((a, i) => {
1065
+ const date = a.created_at ? a.created_at.substring(0, 10) : "";
1066
+ log(` ${colors.cyan}${i + 1}${colors.reset}) ${a.name} ${colors.dim}(${date})${colors.reset}`);
1067
+ });
1068
+ }
1069
+ log(` ${colors.cyan}0${colors.reset}) \uC0C8 \uC571 \uB9CC\uB4E4\uAE30`);
1070
+ const choice = await prompt(`
1071
+ ${colors.blue}?${colors.reset} \uC571 \uC120\uD0DD (\uBC88\uD638): `);
1072
+ const num = parseInt(choice, 10);
1073
+ if (num > 0 && num <= apps.length) {
1074
+ success(`\uC120\uD0DD\uB428: ${apps[num - 1].name}`);
1075
+ return apps[num - 1].id;
1076
+ }
1077
+ const projectName = path.basename(process.cwd());
1078
+ const appName = await prompt(`${colors.blue}?${colors.reset} \uC571 \uC774\uB984 (${projectName}): `) || projectName;
1079
+ info("\uC571 \uC0DD\uC131 \uC911...");
1080
+ const createRes = await makeRequest(
1081
+ `${baseUrl}/v1/public/cli/apps`,
1082
+ "POST",
1083
+ { "X-API-Key": apiKey },
1084
+ JSON.stringify({ name: appName })
1085
+ );
1086
+ if (createRes.status === 402) {
1087
+ error("\uC571 \uC0DD\uC131 \uD55C\uB3C4 \uCD08\uACFC. \uD50C\uB79C \uC5C5\uADF8\uB808\uC774\uB4DC\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4");
1088
+ process.exit(1);
1089
+ }
1090
+ if (createRes.status !== 201) {
1091
+ const data = createRes.data;
1092
+ error(`\uC571 \uC0DD\uC131 \uC2E4\uD328: ${data?.error || `HTTP ${createRes.status}`}`);
1093
+ process.exit(1);
1094
+ }
1095
+ const createData = createRes.data;
1096
+ success(`\uC571 \uC0DD\uC131 \uC644\uB8CC: ${createData.app_name}`);
1097
+ return createData.app_id;
1098
+ }
952
1099
  async function startTunnel(port, config, tunnelOpts) {
953
1100
  if (!config.apiKey) {
954
- error("API Key\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. -k \uC635\uC158 \uB610\uB294 CONNECTBASE_API_KEY \uD658\uACBD\uBCC0\uC218\uB97C \uC124\uC815\uD558\uC138\uC694");
1101
+ error("Secret Key\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. -k \uC635\uC158 \uB610\uB294 CONNECTBASE_API_KEY \uD658\uACBD\uBCC0\uC218\uB97C \uC124\uC815\uD558\uC138\uC694");
1102
+ info("Secret Key\uB294 \uCF58\uC194 > \uD504\uB85C\uD544 > \uC2DC\uD06C\uB9BF \uD0A4 \uD0ED\uC5D0\uC11C \uC0DD\uC131\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4");
955
1103
  process.exit(1);
956
1104
  }
1105
+ if (!config.apiKey.startsWith("cb_sk_")) {
1106
+ error("\uD130\uB110\uC740 \uC720\uC800 Secret Key(cb_sk_)\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. Public Key(cb_pk_)\uB294 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
1107
+ info("Secret Key\uB294 \uCF58\uC194 > \uD504\uB85C\uD544 > \uC2DC\uD06C\uB9BF \uD0A4 \uD0ED\uC5D0\uC11C \uC0DD\uC131\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4");
1108
+ process.exit(1);
1109
+ }
1110
+ const appId = await resolveAppForTunnel(config.apiKey, config.baseUrl, tunnelOpts?.appId);
957
1111
  const tunnelServerUrl = getTunnelServerUrl(config.baseUrl);
958
1112
  const parsedUrl = new URL(tunnelServerUrl);
959
1113
  const isHttps = parsedUrl.protocol === "https:";
960
- let wsPath = `/v1/tunnel/connect?api_key=${encodeURIComponent(config.apiKey)}`;
1114
+ let wsPath = `/v1/tunnel/connect?app_id=${encodeURIComponent(appId)}`;
961
1115
  if (tunnelOpts?.timeout) {
962
1116
  wsPath += `&timeout=${tunnelOpts.timeout}`;
963
1117
  }
@@ -1003,7 +1157,8 @@ ${colors.cyan}ConnectBase Tunnel${colors.reset}`);
1003
1157
  "Upgrade": "websocket",
1004
1158
  "Connection": "Upgrade",
1005
1159
  "Sec-WebSocket-Key": wsKey,
1006
- "Sec-WebSocket-Version": "13"
1160
+ "Sec-WebSocket-Version": "13",
1161
+ "Authorization": `Bearer ${config.apiKey}`
1007
1162
  }
1008
1163
  };
1009
1164
  const req = lib.request(reqOptions);
@@ -1129,29 +1284,78 @@ ${colors.dim}Ctrl+C\uB85C \uC885\uB8CC${colors.reset}
1129
1284
  headers: localHeaders
1130
1285
  };
1131
1286
  const localReq = http.request(reqOptions, (res) => {
1132
- const chunks = [];
1133
- res.on("data", (chunk) => chunks.push(chunk));
1134
- res.on("end", () => {
1135
- const body = Buffer.concat(chunks);
1136
- const responseHeaders = {};
1137
- for (const [key, value] of Object.entries(res.headers)) {
1138
- if (value) responseHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
1139
- }
1140
- const response = {
1141
- type: "http_response",
1142
- request_id: requestId,
1143
- status: res.statusCode || 200,
1144
- headers: responseHeaders,
1145
- body: body.length > 0 ? body.toString("base64") : ""
1146
- };
1287
+ const responseHeaders = {};
1288
+ for (const [key, value] of Object.entries(res.headers)) {
1289
+ if (value) responseHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
1290
+ }
1291
+ const contentType = (responseHeaders["content-type"] || "").toLowerCase();
1292
+ const transferEncoding = (responseHeaders["transfer-encoding"] || "").toLowerCase();
1293
+ const isStreaming = contentType.includes("text/event-stream") || transferEncoding.includes("chunked");
1294
+ if (isStreaming) {
1147
1295
  try {
1148
- sock.write(createWsTextFrame(JSON.stringify(response)));
1149
- const methodColor = method === "GET" ? colors.green : method === "POST" ? colors.blue : colors.yellow;
1150
- log(`${colors.dim}${(/* @__PURE__ */ new Date()).toLocaleTimeString()}${colors.reset} ${methodColor}${method}${colors.reset} ${reqPath} \u2192 ${res.statusCode}`);
1296
+ sock.write(createWsTextFrame(JSON.stringify({
1297
+ type: "http_response_start",
1298
+ request_id: requestId,
1299
+ status: res.statusCode || 200,
1300
+ headers: responseHeaders
1301
+ })));
1151
1302
  } catch {
1152
- warn(`\uC751\uB2F5 \uC804\uC1A1 \uC2E4\uD328: ${requestId}`);
1303
+ warn(`\uC2A4\uD2B8\uB9AC\uBC0D \uC2DC\uC791 \uC804\uC1A1 \uC2E4\uD328: ${requestId}`);
1304
+ return;
1153
1305
  }
1154
- });
1306
+ const methodColor = method === "GET" ? colors.green : method === "POST" ? colors.blue : colors.yellow;
1307
+ log(`${colors.dim}${(/* @__PURE__ */ new Date()).toLocaleTimeString()}${colors.reset} ${methodColor}${method}${colors.reset} ${reqPath} \u2192 ${res.statusCode} ${colors.cyan}[stream]${colors.reset}`);
1308
+ res.on("data", (chunk) => {
1309
+ try {
1310
+ sock.write(createWsTextFrame(JSON.stringify({
1311
+ type: "http_response_chunk",
1312
+ request_id: requestId,
1313
+ data: chunk.toString("base64")
1314
+ })));
1315
+ } catch {
1316
+ warn(`\uC2A4\uD2B8\uB9AC\uBC0D \uCCAD\uD06C \uC804\uC1A1 \uC2E4\uD328: ${requestId}`);
1317
+ }
1318
+ });
1319
+ res.on("end", () => {
1320
+ try {
1321
+ sock.write(createWsTextFrame(JSON.stringify({
1322
+ type: "http_response_end",
1323
+ request_id: requestId
1324
+ })));
1325
+ } catch {
1326
+ }
1327
+ });
1328
+ res.on("error", (err) => {
1329
+ try {
1330
+ sock.write(createWsTextFrame(JSON.stringify({
1331
+ type: "http_response_error",
1332
+ request_id: requestId,
1333
+ error: err.message
1334
+ })));
1335
+ } catch {
1336
+ }
1337
+ });
1338
+ } else {
1339
+ const chunks = [];
1340
+ res.on("data", (chunk) => chunks.push(chunk));
1341
+ res.on("end", () => {
1342
+ const body = Buffer.concat(chunks);
1343
+ const response = {
1344
+ type: "http_response",
1345
+ request_id: requestId,
1346
+ status: res.statusCode || 200,
1347
+ headers: responseHeaders,
1348
+ body: body.length > 0 ? body.toString("base64") : ""
1349
+ };
1350
+ try {
1351
+ sock.write(createWsTextFrame(JSON.stringify(response)));
1352
+ const methodColor = method === "GET" ? colors.green : method === "POST" ? colors.blue : colors.yellow;
1353
+ log(`${colors.dim}${(/* @__PURE__ */ new Date()).toLocaleTimeString()}${colors.reset} ${methodColor}${method}${colors.reset} ${reqPath} \u2192 ${res.statusCode}`);
1354
+ } catch {
1355
+ warn(`\uC751\uB2F5 \uC804\uC1A1 \uC2E4\uD328: ${requestId}`);
1356
+ }
1357
+ });
1358
+ }
1155
1359
  });
1156
1360
  localReq.on("error", (err) => {
1157
1361
  const response = {
@@ -1261,6 +1465,8 @@ function parseArgs(args) {
1261
1465
  result.options.timeout = args[++i];
1262
1466
  } else if (arg === "--max-body") {
1263
1467
  result.options.maxBody = args[++i];
1468
+ } else if (arg === "-a" || arg === "--app") {
1469
+ result.options.appId = args[++i];
1264
1470
  } else if (arg === "-d" || arg === "--dev") {
1265
1471
  result.options.dev = "true";
1266
1472
  } else if (arg === "-h" || arg === "--help") {
@@ -1340,6 +1546,9 @@ async function main() {
1340
1546
  const m = parseInt(parsed.options.maxBody, 10);
1341
1547
  if (!isNaN(m) && m > 0) tunnelOpts.maxBody = m;
1342
1548
  }
1549
+ if (parsed.options.appId) {
1550
+ tunnelOpts.appId = parsed.options.appId;
1551
+ }
1343
1552
  await startTunnel(port, config, tunnelOpts);
1344
1553
  } else {
1345
1554
  error(`\uC54C \uC218 \uC5C6\uB294 \uBA85\uB839\uC5B4: ${parsed.command}`);