junis 0.2.0 → 0.2.3

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/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "junis",
34
- version: "0.2.0",
34
+ version: "0.2.3",
35
35
  description: "One-line device control for AI agents",
36
36
  bin: {
37
37
  junis: "dist/cli/index.js"
@@ -87,8 +87,15 @@ function loadConfig() {
87
87
  }
88
88
  }
89
89
  function saveConfig(config) {
90
- import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true });
91
- import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
90
+ try {
91
+ import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true });
92
+ import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
93
+ } catch (err) {
94
+ console.error(`
95
+ \u274C \uC124\uC815 \uD30C\uC77C \uC800\uC7A5 \uC2E4\uD328: ${err.message}`);
96
+ console.error(` \uC218\uB3D9\uC73C\uB85C ${CONFIG_FILE} \uC5D0 \uC800\uC7A5\uD574\uC8FC\uC138\uC694.`);
97
+ console.error(` \uB0B4\uC6A9: ${JSON.stringify(config, null, 2)}`);
98
+ }
92
99
  }
93
100
  function clearConfig() {
94
101
  if (import_fs.default.existsSync(CONFIG_FILE)) {
@@ -113,11 +120,19 @@ var JUNIS_WEB = (() => {
113
120
  return null;
114
121
  })();
115
122
  async function authenticate(deviceName, platform2, onBrowserOpen, onWaiting) {
116
- const startRes = await fetch(`${JUNIS_API}/api/auth/device/start`, {
117
- method: "POST",
118
- headers: { "Content-Type": "application/json" },
119
- body: JSON.stringify({ device_name: deviceName, platform: platform2 })
120
- });
123
+ let startRes;
124
+ try {
125
+ startRes = await fetch(`${JUNIS_API}/api/auth/device/start`, {
126
+ method: "POST",
127
+ headers: { "Content-Type": "application/json" },
128
+ body: JSON.stringify({ device_name: deviceName, platform: platform2 })
129
+ });
130
+ } catch (err) {
131
+ throw new Error(
132
+ `\uC11C\uBC84\uC5D0 \uC5F0\uACB0\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC778\uD130\uB137 \uC5F0\uACB0\uC744 \uD655\uC778\uD558\uAC70\uB098 \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.
133
+ (${err.message})`
134
+ );
135
+ }
121
136
  if (!startRes.ok) {
122
137
  const body = await startRes.text().catch(() => "");
123
138
  throw new Error(`Auth \uC2DC\uC791 \uC2E4\uD328: ${startRes.status} ${body}`);
@@ -125,15 +140,29 @@ async function authenticate(deviceName, platform2, onBrowserOpen, onWaiting) {
125
140
  const startData = await startRes.json();
126
141
  const verificationUri = JUNIS_WEB ? startData.verification_uri.replace(/^https?:\/\/[^/]+/, JUNIS_WEB) : startData.verification_uri;
127
142
  onBrowserOpen?.(verificationUri);
128
- await (0, import_open.default)(verificationUri);
143
+ try {
144
+ await (0, import_open.default)(verificationUri);
145
+ } catch {
146
+ console.warn(`
147
+ \u26A0\uFE0F \uBE0C\uB77C\uC6B0\uC800\uB97C \uC790\uB3D9\uC73C\uB85C \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC544\uB798 URL\uC744 \uC9C1\uC811 \uC5F4\uC5B4\uC8FC\uC138\uC694:
148
+
149
+ ${verificationUri}
150
+ `);
151
+ }
129
152
  const deadline = Date.now() + startData.expires_in * 1e3;
130
153
  const intervalMs = startData.interval * 1e3;
131
154
  let waitingCallbackCalled = false;
132
155
  while (Date.now() < deadline) {
133
156
  await sleep(intervalMs);
134
- const pollRes = await fetch(
135
- `${JUNIS_API}/api/auth/device/poll?code=${startData.device_code}`
136
- );
157
+ let pollRes;
158
+ try {
159
+ pollRes = await fetch(
160
+ `${JUNIS_API}/api/auth/device/poll?code=${startData.device_code}`
161
+ );
162
+ } catch {
163
+ onWaiting?.();
164
+ continue;
165
+ }
137
166
  if (pollRes.status === 202) {
138
167
  if (!waitingCallbackCalled) {
139
168
  waitingCallbackCalled = true;
@@ -184,15 +213,18 @@ var RelayClient = class {
184
213
  if (this.destroyed) return;
185
214
  const url = `${JUNIS_WS}/ws/devices/${this.config.device_key}`;
186
215
  console.log(`\u{1F517} \uB9B4\uB808\uC774 \uC11C\uBC84 \uC5F0\uACB0 \uC911...`);
187
- this.ws = new import_ws.default(url, {
216
+ const ws = new import_ws.default(url, {
188
217
  headers: { Authorization: `Bearer ${this.config.token}` }
189
218
  });
190
- this.ws.on("open", () => {
219
+ this.ws = ws;
220
+ ws.on("open", () => {
221
+ if (this.ws !== ws) return;
191
222
  console.log("\u2705 \uB9B4\uB808\uC774 \uC11C\uBC84 \uC5F0\uACB0\uB428");
192
223
  this.reconnectDelay = 1e3;
193
224
  this.startHeartbeat();
194
225
  });
195
- this.ws.on("message", async (raw) => {
226
+ ws.on("message", async (raw) => {
227
+ if (this.ws !== ws) return;
196
228
  try {
197
229
  const msg = JSON.parse(raw.toString());
198
230
  if (msg.type === "pong") return;
@@ -211,9 +243,10 @@ var RelayClient = class {
211
243
  } catch {
212
244
  }
213
245
  });
214
- this.ws.on("close", async (code) => {
215
- this.stopHeartbeat();
246
+ ws.on("close", async (code) => {
216
247
  if (this.destroyed) return;
248
+ if (this.ws !== ws) return;
249
+ this.stopHeartbeat();
217
250
  if (code === 4001) {
218
251
  this.destroyed = true;
219
252
  if (this.onAuthExpired) {
@@ -230,7 +263,7 @@ var RelayClient = class {
230
263
  setTimeout(() => this.connect(), this.reconnectDelay);
231
264
  this.reconnectDelay = Math.min(this.reconnectDelay * 2, 3e4);
232
265
  });
233
- this.ws.on("error", (err) => {
266
+ ws.on("error", (err) => {
234
267
  console.error(`\uB9B4\uB808\uC774 \uC624\uB958: ${err.message}`);
235
268
  });
236
269
  }
@@ -276,6 +309,7 @@ var import_path2 = __toESM(require("path"));
276
309
  var import_glob = require("glob");
277
310
  var import_zod = require("zod");
278
311
  var execAsync = (0, import_util.promisify)(import_child_process.exec);
312
+ var execFileAsync = (0, import_util.promisify)(import_child_process.execFile);
279
313
  var FilesystemTools = class {
280
314
  register(server) {
281
315
  server.tool(
@@ -321,8 +355,16 @@ ${error.stderr ?? ""}`
321
355
  encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("\uC778\uCF54\uB529")
322
356
  },
323
357
  async ({ path: filePath, encoding }) => {
324
- const content = await import_promises.default.readFile(filePath, encoding);
325
- return { content: [{ type: "text", text: content }] };
358
+ try {
359
+ const content = await import_promises.default.readFile(filePath, encoding);
360
+ return { content: [{ type: "text", text: content }] };
361
+ } catch (err) {
362
+ const e = err;
363
+ if (e.code === "ENOENT") {
364
+ return { content: [{ type: "text", text: `\u274C \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath}` }], isError: true };
365
+ }
366
+ return { content: [{ type: "text", text: `\u274C \uD30C\uC77C \uC77D\uAE30 \uC2E4\uD328: ${e.message}` }], isError: true };
367
+ }
326
368
  }
327
369
  );
328
370
  server.tool(
@@ -345,9 +387,17 @@ ${error.stderr ?? ""}`
345
387
  path: import_zod.z.string().describe("\uB514\uB809\uD1A0\uB9AC \uACBD\uB85C")
346
388
  },
347
389
  async ({ path: dirPath }) => {
348
- const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true });
349
- const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
350
- return { content: [{ type: "text", text: lines.join("\n") }] };
390
+ try {
391
+ const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true });
392
+ const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
393
+ return { content: [{ type: "text", text: lines.join("\n") }] };
394
+ } catch (err) {
395
+ const e = err;
396
+ if (e.code === "ENOENT") {
397
+ return { content: [{ type: "text", text: `\u274C \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${dirPath}` }], isError: true };
398
+ }
399
+ return { content: [{ type: "text", text: `\u274C \uB514\uB809\uD1A0\uB9AC \uC77D\uAE30 \uC2E4\uD328: ${e.message}` }], isError: true };
400
+ }
351
401
  }
352
402
  );
353
403
  server.tool(
@@ -360,18 +410,20 @@ ${error.stderr ?? ""}`
360
410
  },
361
411
  async ({ pattern, directory, file_pattern }) => {
362
412
  try {
363
- const { stdout } = await execAsync(
364
- `rg --no-heading -n "${pattern}" ${directory}`,
413
+ const { stdout } = await execFileAsync(
414
+ "rg",
415
+ ["--no-heading", "-n", pattern, directory],
365
416
  { timeout: 1e4 }
366
417
  );
367
418
  return { content: [{ type: "text", text: stdout || "\uACB0\uACFC \uC5C6\uC74C" }] };
368
419
  } catch {
369
- const files = await (0, import_glob.glob)(file_pattern, { cwd: directory });
420
+ const safeDirectory = import_path2.default.resolve(directory);
421
+ const files = await (0, import_glob.glob)(file_pattern, { cwd: safeDirectory });
370
422
  const results = [];
371
423
  for (const file of files.slice(0, 100)) {
372
424
  try {
373
425
  const content = await import_promises.default.readFile(
374
- import_path2.default.join(directory, file),
426
+ import_path2.default.join(safeDirectory, file),
375
427
  "utf-8"
376
428
  );
377
429
  const lines = content.split("\n");
@@ -498,6 +550,17 @@ var import_zod2 = require("zod");
498
550
  var BrowserTools = class {
499
551
  browser = null;
500
552
  page = null;
553
+ // 동시 요청 시 race condition 방지용 직렬화 락
554
+ lock = Promise.resolve();
555
+ withLock(fn) {
556
+ let release;
557
+ const next = new Promise((r) => {
558
+ release = r;
559
+ });
560
+ const current = this.lock;
561
+ this.lock = this.lock.then(() => next);
562
+ return current.then(() => fn()).finally(() => release());
563
+ }
501
564
  async init() {
502
565
  try {
503
566
  this.browser = await import_playwright.chromium.launch({ headless: true });
@@ -520,22 +583,22 @@ var BrowserTools = class {
520
583
  "browser_navigate",
521
584
  "URL\uB85C \uC774\uB3D9",
522
585
  { url: import_zod2.z.string().describe("\uC774\uB3D9\uD560 URL") },
523
- async ({ url }) => {
586
+ ({ url }) => this.withLock(async () => {
524
587
  const page = requirePage();
525
588
  await page.goto(url, { waitUntil: "domcontentloaded" });
526
589
  return {
527
590
  content: [{ type: "text", text: `\uC774\uB3D9 \uC644\uB8CC: ${page.url()}` }]
528
591
  };
529
- }
592
+ })
530
593
  );
531
594
  server.tool(
532
595
  "browser_click",
533
596
  "\uC694\uC18C \uD074\uB9AD",
534
597
  { selector: import_zod2.z.string().describe("CSS \uC120\uD0DD\uC790") },
535
- async ({ selector }) => {
598
+ ({ selector }) => this.withLock(async () => {
536
599
  await requirePage().click(selector);
537
600
  return { content: [{ type: "text", text: "\uD074\uB9AD \uC644\uB8CC" }] };
538
- }
601
+ })
539
602
  );
540
603
  server.tool(
541
604
  "browser_type",
@@ -545,12 +608,12 @@ var BrowserTools = class {
545
608
  text: import_zod2.z.string().describe("\uC785\uB825\uD560 \uD14D\uC2A4\uD2B8"),
546
609
  clear: import_zod2.z.boolean().optional().default(false).describe("\uAE30\uC874 \uB0B4\uC6A9 \uC0AD\uC81C \uD6C4 \uC785\uB825")
547
610
  },
548
- async ({ selector, text, clear }) => {
611
+ ({ selector, text, clear }) => this.withLock(async () => {
549
612
  const page = requirePage();
550
613
  if (clear) await page.fill(selector, text);
551
614
  else await page.type(selector, text);
552
615
  return { content: [{ type: "text", text: "\uC785\uB825 \uC644\uB8CC" }] };
553
- }
616
+ })
554
617
  );
555
618
  server.tool(
556
619
  "browser_screenshot",
@@ -559,14 +622,14 @@ var BrowserTools = class {
559
622
  path: import_zod2.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (\uC5C6\uC73C\uBA74 base64 \uBC18\uD658)"),
560
623
  full_page: import_zod2.z.boolean().optional().default(false)
561
624
  },
562
- async ({ path: path3, full_page }) => {
625
+ ({ path: path4, full_page }) => this.withLock(async () => {
563
626
  const page = requirePage();
564
627
  const screenshot = await page.screenshot({
565
- path: path3 ?? void 0,
628
+ path: path4 ?? void 0,
566
629
  fullPage: full_page
567
630
  });
568
- if (path3) {
569
- return { content: [{ type: "text", text: `\uC800\uC7A5 \uC644\uB8CC: ${path3}` }] };
631
+ if (path4) {
632
+ return { content: [{ type: "text", text: `\uC800\uC7A5 \uC644\uB8CC: ${path4}` }] };
570
633
  }
571
634
  return {
572
635
  content: [
@@ -577,13 +640,13 @@ var BrowserTools = class {
577
640
  }
578
641
  ]
579
642
  };
580
- }
643
+ })
581
644
  );
582
645
  server.tool(
583
646
  "browser_snapshot",
584
647
  "\uD398\uC774\uC9C0 \uC811\uADFC\uC131 \uD2B8\uB9AC \uC870\uD68C (\uAD6C\uC870 \uD30C\uC545\uC6A9)",
585
648
  {},
586
- async () => {
649
+ () => this.withLock(async () => {
587
650
  const page = requirePage();
588
651
  const snapshot = await page.locator("body").ariaSnapshot();
589
652
  return {
@@ -591,29 +654,36 @@ var BrowserTools = class {
591
654
  { type: "text", text: snapshot }
592
655
  ]
593
656
  };
594
- }
657
+ })
595
658
  );
596
659
  server.tool(
597
660
  "browser_evaluate",
598
661
  "JavaScript \uC2E4\uD589",
599
662
  { code: import_zod2.z.string().describe("\uC2E4\uD589\uD560 JavaScript \uCF54\uB4DC") },
600
- async ({ code }) => {
601
- const result = await requirePage().evaluate(code);
602
- return {
603
- content: [
604
- { type: "text", text: JSON.stringify(result, null, 2) }
605
- ]
606
- };
607
- }
663
+ ({ code }) => this.withLock(async () => {
664
+ try {
665
+ const result = await requirePage().evaluate(code);
666
+ return {
667
+ content: [
668
+ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }
669
+ ]
670
+ };
671
+ } catch (err) {
672
+ return {
673
+ content: [{ type: "text", text: `\u274C JavaScript \uC2E4\uD589 \uC624\uB958: ${err.message}` }],
674
+ isError: true
675
+ };
676
+ }
677
+ })
608
678
  );
609
679
  server.tool(
610
680
  "browser_pdf",
611
681
  "\uD604\uC7AC \uD398\uC774\uC9C0 PDF \uC800\uC7A5",
612
682
  { path: import_zod2.z.string().describe("\uC800\uC7A5 \uACBD\uB85C (.pdf)") },
613
- async ({ path: path3 }) => {
614
- await requirePage().pdf({ path: path3 });
615
- return { content: [{ type: "text", text: `PDF \uC800\uC7A5 \uC644\uB8CC: ${path3}` }] };
616
- }
683
+ ({ path: path4 }) => this.withLock(async () => {
684
+ await requirePage().pdf({ path: path4 });
685
+ return { content: [{ type: "text", text: `PDF \uC800\uC7A5 \uC644\uB8CC: ${path4}` }] };
686
+ })
617
687
  );
618
688
  }
619
689
  };
@@ -626,7 +696,11 @@ var import_util2 = require("util");
626
696
  var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
627
697
  async function readNotebook(filePath) {
628
698
  const raw = await import_promises2.default.readFile(filePath, "utf-8");
629
- return JSON.parse(raw);
699
+ try {
700
+ return JSON.parse(raw);
701
+ } catch {
702
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 Jupyter \uB178\uD2B8\uBD81 \uD30C\uC77C\uC785\uB2C8\uB2E4: ${filePath}`);
703
+ }
630
704
  }
631
705
  async function writeNotebook(filePath, nb) {
632
706
  await import_promises2.default.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
@@ -721,15 +795,21 @@ var NotebookTools = class {
721
795
  execution_count: cellType === "code" ? null : void 0
722
796
  };
723
797
  let actualIndex;
798
+ let warning = "";
724
799
  if (position === void 0 || position === null) {
725
800
  nb.cells.push(newCell);
726
801
  actualIndex = nb.cells.length - 1;
802
+ } else if (position > nb.cells.length) {
803
+ nb.cells.push(newCell);
804
+ actualIndex = nb.cells.length - 1;
805
+ warning = ` (\uACBD\uACE0: position ${position}\uC774 \uBC94\uC704\uB97C \uCD08\uACFC\uD558\uC5EC \uB05D(index: ${actualIndex})\uC5D0 \uCD94\uAC00\uB428)`;
727
806
  } else {
728
- nb.cells.splice(position, 0, newCell);
729
- actualIndex = position;
807
+ const clamped = Math.max(0, position);
808
+ nb.cells.splice(clamped, 0, newCell);
809
+ actualIndex = clamped;
730
810
  }
731
811
  await writeNotebook(filePath, nb);
732
- return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})` }] };
812
+ return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})${warning}` }] };
733
813
  }
734
814
  );
735
815
  server.tool(
@@ -757,13 +837,13 @@ var import_child_process3 = require("child_process");
757
837
  var import_util3 = require("util");
758
838
  var import_zod4 = require("zod");
759
839
  var execAsync3 = (0, import_util3.promisify)(import_child_process3.exec);
840
+ var screenRecordPid = null;
760
841
  function platform() {
761
842
  if (process.platform === "darwin") return "mac";
762
843
  if (process.platform === "win32") return "win";
763
844
  return "linux";
764
845
  }
765
846
  var DeviceTools = class {
766
- screenRecordPid = null;
767
847
  register(server) {
768
848
  server.tool(
769
849
  "screen_capture",
@@ -773,15 +853,26 @@ var DeviceTools = class {
773
853
  },
774
854
  async ({ output_path }) => {
775
855
  const p = platform();
856
+ const isTmp = !output_path;
776
857
  const tmpPath = output_path ?? `/tmp/junis_screen_${Date.now()}.png`;
777
858
  const cmd = {
778
859
  mac: `screencapture -x "${tmpPath}"`,
779
860
  win: `nircmd.exe savescreenshot "${tmpPath}"`,
780
861
  linux: `scrot "${tmpPath}"`
781
862
  }[p];
782
- await execAsync3(cmd);
783
- const { readFileSync } = await import("fs");
863
+ try {
864
+ await execAsync3(cmd);
865
+ } catch (err) {
866
+ throw new Error(`\uD654\uBA74 \uCEA1\uCC98 \uC2E4\uD328: ${err.message}`);
867
+ }
868
+ const { readFileSync, unlinkSync } = await import("fs");
784
869
  const data = readFileSync(tmpPath).toString("base64");
870
+ if (isTmp) {
871
+ try {
872
+ unlinkSync(tmpPath);
873
+ } catch {
874
+ }
875
+ }
785
876
  return {
786
877
  content: [{ type: "image", data, mimeType: "image/png" }]
787
878
  };
@@ -795,15 +886,31 @@ var DeviceTools = class {
795
886
  },
796
887
  async ({ output_path }) => {
797
888
  const p = platform();
889
+ const isTmp = !output_path;
798
890
  const tmpPath = output_path ?? `/tmp/junis_cam_${Date.now()}.jpg`;
799
891
  const cmd = {
800
892
  mac: `imagesnap "${tmpPath}"`,
801
893
  win: `ffmpeg -f dshow -i video="Default" -frames:v 1 "${tmpPath}"`,
802
894
  linux: `fswebcam -r 1280x720 "${tmpPath}"`
803
895
  }[p];
804
- await execAsync3(cmd);
805
- const { readFileSync } = await import("fs");
896
+ try {
897
+ await execAsync3(cmd);
898
+ } catch (err) {
899
+ const e = err;
900
+ return {
901
+ content: [{ type: "text", text: `\u274C \uCE74\uBA54\uB77C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uAC70\uB098 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
902
+ \uC6D0\uC778: ${e.message}
903
+
904
+ \uCE74\uBA54\uB77C\uAC00 \uC5F0\uACB0\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.` }],
905
+ isError: true
906
+ };
907
+ }
908
+ const { readFileSync, unlinkSync } = await import("fs");
806
909
  const data = readFileSync(tmpPath).toString("base64");
910
+ if (isTmp) try {
911
+ unlinkSync(tmpPath);
912
+ } catch {
913
+ }
807
914
  return {
808
915
  content: [{ type: "image", data, mimeType: "image/jpeg" }]
809
916
  };
@@ -818,11 +925,17 @@ var DeviceTools = class {
818
925
  },
819
926
  async ({ title, message }) => {
820
927
  const p = platform();
821
- const cmd = {
822
- mac: `osascript -e 'display notification "${message}" with title "${title}"'`,
823
- win: `powershell -Command "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime]::CreateToastNotifier('Junis').Show([Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType=WindowsRuntime]::new())"`,
824
- linux: `notify-send "${title}" "${message}"`
825
- }[p];
928
+ let cmd;
929
+ if (p === "win") {
930
+ const script = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${message.replace(/'/g, "''")}', '${title.replace(/'/g, "''")}')`;
931
+ const encoded = Buffer.from(script, "utf16le").toString("base64");
932
+ cmd = `powershell -NoProfile -EncodedCommand ${encoded}`;
933
+ } else {
934
+ cmd = {
935
+ mac: `osascript -e 'display notification "${message.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"'`,
936
+ linux: `notify-send "${title.replace(/"/g, '\\"')}" "${message.replace(/"/g, '\\"')}"`
937
+ }[p] ?? "";
938
+ }
826
939
  await execAsync3(cmd);
827
940
  return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
828
941
  }
@@ -863,25 +976,26 @@ var DeviceTools = class {
863
976
  async ({ action, output_path }) => {
864
977
  const p = platform();
865
978
  if (action === "start") {
866
- if (this.screenRecordPid) {
979
+ if (screenRecordPid) {
867
980
  return { content: [{ type: "text", text: "\uC774\uBBF8 \uB179\uD654 \uC911\uC785\uB2C8\uB2E4." }] };
868
981
  }
869
982
  const tmpPath = output_path ?? `/tmp/junis_record_${Date.now()}.mp4`;
870
- const { spawn } = await import("child_process");
983
+ const { spawn: spawn2 } = await import("child_process");
871
984
  const cmd = p === "mac" ? ["screencapture", ["-v", tmpPath]] : ["ffmpeg", ["-f", p === "win" ? "gdigrab" : "x11grab", "-i", p === "win" ? "desktop" : ":0.0", tmpPath]];
872
- const child = spawn(cmd[0], cmd[1], { detached: true, stdio: "ignore" });
985
+ const child = spawn2(cmd[0], cmd[1], { detached: true, stdio: "ignore" });
873
986
  child.unref();
874
- this.screenRecordPid = child.pid ?? null;
875
- return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${this.screenRecordPid})` }] };
987
+ screenRecordPid = child.pid ?? null;
988
+ return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${screenRecordPid})` }] };
876
989
  } else {
877
- if (!this.screenRecordPid) {
990
+ if (!screenRecordPid) {
878
991
  return { content: [{ type: "text", text: "\uD604\uC7AC \uB179\uD654 \uC911\uC774 \uC544\uB2D9\uB2C8\uB2E4." }] };
879
992
  }
880
993
  try {
881
- process.kill(this.screenRecordPid, "SIGINT");
994
+ process.kill(screenRecordPid, "SIGINT");
995
+ await new Promise((r) => setTimeout(r, 1e3));
882
996
  } catch {
883
997
  }
884
- this.screenRecordPid = null;
998
+ screenRecordPid = null;
885
999
  return { content: [{ type: "text", text: "\uB179\uD654 \uC911\uC9C0\uB428." }] };
886
1000
  }
887
1001
  }
@@ -900,12 +1014,15 @@ var DeviceTools = class {
900
1014
  } catch {
901
1015
  }
902
1016
  }
903
- const res = await fetch("https://ipapi.co/json/");
1017
+ const res = await fetch("http://ip-api.com/json/");
904
1018
  const data = await res.json();
1019
+ if (data.status !== "success") {
1020
+ throw new Error(`IP \uC704\uCE58 \uC870\uD68C \uC2E4\uD328: ${data.message ?? data.status}`);
1021
+ }
905
1022
  return {
906
1023
  content: [{
907
1024
  type: "text",
908
- text: `\uC704\uB3C4: ${data.latitude}, \uACBD\uB3C4: ${data.longitude}, \uB3C4\uC2DC: ${data.city}, \uAD6D\uAC00: ${data.country_name} (IP \uAE30\uBC18 \uCD94\uC815)`
1025
+ text: `\uC704\uB3C4: ${data.lat}, \uACBD\uB3C4: ${data.lon}, \uB3C4\uC2DC: ${data.city}, \uAD6D\uAC00: ${data.country} (IP \uAE30\uBC18 \uCD94\uC815)`
909
1026
  }]
910
1027
  };
911
1028
  }
@@ -1178,6 +1295,171 @@ async function handleMCPRequest(id, payload) {
1178
1295
  return null;
1179
1296
  }
1180
1297
 
1298
+ // src/cli/daemon.ts
1299
+ var import_fs2 = __toESM(require("fs"));
1300
+ var import_path3 = __toESM(require("path"));
1301
+ var import_os2 = __toESM(require("os"));
1302
+ var import_child_process4 = require("child_process");
1303
+ var CONFIG_DIR2 = import_path3.default.join(import_os2.default.homedir(), ".junis");
1304
+ var PID_FILE = import_path3.default.join(CONFIG_DIR2, "junis.pid");
1305
+ var LOG_DIR = import_path3.default.join(CONFIG_DIR2, "logs");
1306
+ var LOG_FILE = import_path3.default.join(LOG_DIR, "junis.log");
1307
+ var PLIST_PATH = import_path3.default.join(
1308
+ import_os2.default.homedir(),
1309
+ "Library/LaunchAgents/ai.junis.plist"
1310
+ );
1311
+ var SYSTEMD_PATH = import_path3.default.join(
1312
+ import_os2.default.homedir(),
1313
+ ".config/systemd/user/junis.service"
1314
+ );
1315
+ function isRunning() {
1316
+ try {
1317
+ if (!import_fs2.default.existsSync(PID_FILE)) return { running: false };
1318
+ const pid = parseInt(import_fs2.default.readFileSync(PID_FILE, "utf-8").trim(), 10);
1319
+ if (isNaN(pid)) return { running: false };
1320
+ process.kill(pid, 0);
1321
+ return { running: true, pid };
1322
+ } catch {
1323
+ try {
1324
+ import_fs2.default.unlinkSync(PID_FILE);
1325
+ } catch {
1326
+ }
1327
+ return { running: false };
1328
+ }
1329
+ }
1330
+ function writePid(pid) {
1331
+ import_fs2.default.mkdirSync(CONFIG_DIR2, { recursive: true });
1332
+ import_fs2.default.writeFileSync(PID_FILE, String(pid), "utf-8");
1333
+ }
1334
+ function startDaemon(port) {
1335
+ import_fs2.default.mkdirSync(LOG_DIR, { recursive: true });
1336
+ const nodePath = process.execPath;
1337
+ const scriptPath = process.argv[1];
1338
+ const out = import_fs2.default.openSync(LOG_FILE, "a");
1339
+ const err = import_fs2.default.openSync(LOG_FILE, "a");
1340
+ const child = (0, import_child_process4.spawn)(nodePath, [scriptPath, "start", "--daemon", "--port", String(port)], {
1341
+ detached: true,
1342
+ stdio: ["ignore", out, err]
1343
+ });
1344
+ child.unref();
1345
+ const pid = child.pid;
1346
+ writePid(pid);
1347
+ }
1348
+ function stopDaemon() {
1349
+ const { running, pid } = isRunning();
1350
+ if (!running || !pid) return false;
1351
+ try {
1352
+ process.kill(pid, "SIGTERM");
1353
+ try {
1354
+ import_fs2.default.unlinkSync(PID_FILE);
1355
+ } catch {
1356
+ }
1357
+ return true;
1358
+ } catch {
1359
+ return false;
1360
+ }
1361
+ }
1362
+ var ServiceManager = class {
1363
+ get platform() {
1364
+ if (process.platform === "darwin") return "mac";
1365
+ if (process.platform === "win32") return "win";
1366
+ return "linux";
1367
+ }
1368
+ async install() {
1369
+ const nodePath = process.execPath;
1370
+ const scriptPath = process.argv[1];
1371
+ if (this.platform === "mac") {
1372
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
1373
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1374
+ <plist version="1.0">
1375
+ <dict>
1376
+ <key>Label</key>
1377
+ <string>ai.junis</string>
1378
+ <key>ProgramArguments</key>
1379
+ <array>
1380
+ <string>${nodePath}</string>
1381
+ <string>${scriptPath}</string>
1382
+ <string>start</string>
1383
+ <string>--daemon</string>
1384
+ </array>
1385
+ <key>EnvironmentVariables</key>
1386
+ <dict>
1387
+ <key>HOME</key>
1388
+ <string>${import_os2.default.homedir()}</string>
1389
+ <key>PATH</key>
1390
+ <string>${process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin"}</string>
1391
+ </dict>
1392
+ <key>RunAtLoad</key>
1393
+ <true/>
1394
+ <key>KeepAlive</key>
1395
+ <true/>
1396
+ <key>StandardOutPath</key>
1397
+ <string>${LOG_FILE}</string>
1398
+ <key>StandardErrorPath</key>
1399
+ <string>${LOG_FILE}</string>
1400
+ </dict>
1401
+ </plist>`;
1402
+ import_fs2.default.mkdirSync(import_path3.default.dirname(PLIST_PATH), { recursive: true });
1403
+ import_fs2.default.mkdirSync(LOG_DIR, { recursive: true });
1404
+ import_fs2.default.writeFileSync(PLIST_PATH, plist, "utf-8");
1405
+ try {
1406
+ (0, import_child_process4.execSync)(`launchctl unload "${PLIST_PATH}" 2>/dev/null || true`);
1407
+ (0, import_child_process4.execSync)(`launchctl load "${PLIST_PATH}"`);
1408
+ } catch (e) {
1409
+ throw new Error(`launchctl load \uC2E4\uD328: ${e.message}`);
1410
+ }
1411
+ } else if (this.platform === "linux") {
1412
+ const unit = `[Unit]
1413
+ Description=Junis Device Agent
1414
+ After=network.target
1415
+
1416
+ [Service]
1417
+ ExecStart=${nodePath} ${scriptPath} start --daemon
1418
+ Restart=always
1419
+ RestartSec=5
1420
+ Environment=HOME=${import_os2.default.homedir()}
1421
+ Environment=PATH=${process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin"}
1422
+ StandardOutput=append:${LOG_FILE}
1423
+ StandardError=append:${LOG_FILE}
1424
+
1425
+ [Install]
1426
+ WantedBy=default.target`;
1427
+ import_fs2.default.mkdirSync(import_path3.default.dirname(SYSTEMD_PATH), { recursive: true });
1428
+ import_fs2.default.mkdirSync(LOG_DIR, { recursive: true });
1429
+ import_fs2.default.writeFileSync(SYSTEMD_PATH, unit, "utf-8");
1430
+ (0, import_child_process4.execSync)("systemctl --user daemon-reload");
1431
+ (0, import_child_process4.execSync)("systemctl --user enable junis");
1432
+ (0, import_child_process4.execSync)("systemctl --user start junis");
1433
+ } else {
1434
+ (0, import_child_process4.execSync)(
1435
+ `schtasks /Create /F /TN "Junis" /TR "${nodePath} ${scriptPath} start --daemon" /SC ONLOGON /RL HIGHEST`
1436
+ );
1437
+ }
1438
+ }
1439
+ async uninstall() {
1440
+ if (this.platform === "mac") {
1441
+ try {
1442
+ (0, import_child_process4.execSync)(`launchctl unload "${PLIST_PATH}" 2>/dev/null || true`);
1443
+ if (import_fs2.default.existsSync(PLIST_PATH)) import_fs2.default.unlinkSync(PLIST_PATH);
1444
+ } catch {
1445
+ }
1446
+ } else if (this.platform === "linux") {
1447
+ try {
1448
+ (0, import_child_process4.execSync)("systemctl --user stop junis 2>/dev/null || true");
1449
+ (0, import_child_process4.execSync)("systemctl --user disable junis 2>/dev/null || true");
1450
+ if (import_fs2.default.existsSync(SYSTEMD_PATH)) import_fs2.default.unlinkSync(SYSTEMD_PATH);
1451
+ (0, import_child_process4.execSync)("systemctl --user daemon-reload 2>/dev/null || true");
1452
+ } catch {
1453
+ }
1454
+ } else {
1455
+ try {
1456
+ (0, import_child_process4.execSync)('schtasks /Delete /F /TN "Junis" 2>nul || true');
1457
+ } catch {
1458
+ }
1459
+ }
1460
+ }
1461
+ };
1462
+
1181
1463
  // src/cli/index.ts
1182
1464
  var { version } = require_package();
1183
1465
  import_commander.program.name("junis").description("AI\uAC00 \uB0B4 \uB514\uBC14\uC774\uC2A4\uB97C \uC644\uC804 \uC81C\uC5B4\uD558\uB294 MCP \uC11C\uBC84").version(version);
@@ -1185,9 +1467,9 @@ function getSystemInfo() {
1185
1467
  const platform2 = process.platform;
1186
1468
  if (platform2 === "darwin") {
1187
1469
  try {
1188
- const { execSync } = require("child_process");
1189
- const sw = execSync("sw_vers -productVersion", { encoding: "utf8" }).trim();
1190
- const hw = execSync("sysctl -n machdep.cpu.brand_string", { encoding: "utf8" }).trim();
1470
+ const { execSync: execSync2 } = require("child_process");
1471
+ const sw = execSync2("sw_vers -productVersion", { encoding: "utf8" }).trim();
1472
+ const hw = execSync2("sysctl -n machdep.cpu.brand_string", { encoding: "utf8" }).trim();
1191
1473
  return `macOS ${sw} (${hw})`;
1192
1474
  } catch {
1193
1475
  return "macOS";
@@ -1229,22 +1511,67 @@ function printStep1(port) {
1229
1511
  console.log(` \u25C9 Local MCP endpoint ........... http://localhost:${port}/mcp`);
1230
1512
  console.log("");
1231
1513
  }
1232
- import_commander.program.command("start", { isDefault: true }).description("Junis \uC5D0\uC774\uC804\uD2B8\uC640 \uC5F0\uACB0 \uC2DC\uC791").option("--local", "\uB85C\uCEEC MCP \uC11C\uBC84\uB9CC \uC2E4\uD589 (\uD074\uB77C\uC6B0\uB4DC \uC5F0\uACB0 \uC5C6\uC74C)").option("--port <number>", "\uD3EC\uD2B8 \uBC88\uD638", "3000").option("--reset", "\uAE30\uC874 \uC778\uC99D \uCD08\uAE30\uD654 \uD6C4 \uC7AC\uB85C\uADF8\uC778").action(async (options) => {
1514
+ import_commander.program.command("start", { isDefault: true }).description("Junis \uC5D0\uC774\uC804\uD2B8\uC640 \uC5F0\uACB0 \uC2DC\uC791").option("--local", "\uB85C\uCEEC MCP \uC11C\uBC84\uB9CC \uC2E4\uD589 (\uD074\uB77C\uC6B0\uB4DC \uC5F0\uACB0 \uC5C6\uC74C)").option("--port <number>", "\uD3EC\uD2B8 \uBC88\uD638", "3000").option("--reset", "\uAE30\uC874 \uC778\uC99D \uCD08\uAE30\uD654 \uD6C4 \uC7AC\uB85C\uADF8\uC778").option("--daemon", "\uB370\uBAAC \uBAA8\uB4DC\uB85C \uC2E4\uD589 (\uB0B4\uBD80\uC6A9, launchd/systemd\uC5D0\uC11C \uC0AC\uC6A9)").action(async (options) => {
1233
1515
  const port = parseInt(options.port, 10);
1516
+ if (options.daemon) {
1517
+ if (options.local) {
1518
+ await startMCPServer(port);
1519
+ return;
1520
+ }
1521
+ let config2 = options.reset ? null : loadConfig();
1522
+ if (!config2) {
1523
+ console.error("\u274C \uC778\uC99D \uC815\uBCF4 \uC5C6\uC74C. npx junis \uB97C \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
1524
+ process.exit(1);
1525
+ }
1526
+ const deviceName2 = config2.device_name;
1527
+ const platformName2 = process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux";
1528
+ const actualPort = await startMCPServer(port);
1529
+ console.log(`[junis daemon] MCP server started on port ${actualPort}`);
1530
+ const relay = new RelayClient(config2, handleMCPRequest, async () => {
1531
+ console.log("[junis daemon] \uC138\uC158 \uB9CC\uB8CC - \uC7AC\uC778\uC99D \uD544\uC694");
1532
+ try {
1533
+ let waitingPrinted = false;
1534
+ const authResult = await authenticate(
1535
+ deviceName2,
1536
+ platformName2,
1537
+ (uri) => {
1538
+ console.log(`[junis daemon] \uBE0C\uB77C\uC6B0\uC800 \uC7AC\uC778\uC99D: ${uri}`);
1539
+ },
1540
+ () => {
1541
+ if (!waitingPrinted) waitingPrinted = true;
1542
+ }
1543
+ );
1544
+ config2.token = authResult.token;
1545
+ saveConfig(config2);
1546
+ relay.restart();
1547
+ } catch (e) {
1548
+ console.error("[junis daemon] \uC7AC\uC778\uC99D \uC2E4\uD328:", e);
1549
+ process.exit(1);
1550
+ }
1551
+ });
1552
+ await relay.connect();
1553
+ console.log("[junis daemon] relay connected");
1554
+ return;
1555
+ }
1234
1556
  printBanner();
1235
1557
  if (options.local) {
1236
- const actualPort2 = await startMCPServer(port);
1237
- printStep1(actualPort2);
1558
+ const actualPort = await startMCPServer(port);
1559
+ printStep1(actualPort);
1238
1560
  console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1239
1561
  console.log(" \u2705 ALL SET \u2014 Local MCP server is running");
1240
1562
  console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1241
1563
  return;
1242
1564
  }
1565
+ const { running, pid } = isRunning();
1566
+ if (running) {
1567
+ console.log(`\u2705 Junis \uC2E4\uD589 \uC911\uC785\uB2C8\uB2E4. (PID: ${pid})`);
1568
+ console.log(" \uC885\uB8CC\uD558\uB824\uBA74: npx junis stop");
1569
+ return;
1570
+ }
1243
1571
  let config = options.reset ? null : loadConfig();
1244
1572
  const deviceName = config?.device_name ?? `${process.env["USER"] ?? "user"}'s ${getDeviceName()}`;
1245
1573
  const platformName = process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux";
1246
- const actualPort = await startMCPServer(port);
1247
- printStep1(actualPort);
1574
+ printStep1(port);
1248
1575
  if (!config) {
1249
1576
  let waitingPrinted = false;
1250
1577
  const authResult = await authenticate(
@@ -1305,53 +1632,42 @@ import_commander.program.command("start", { isDefault: true }).description("Juni
1305
1632
  console.log(" \u25C9 Status ....................... \u{1F7E2} online");
1306
1633
  console.log("");
1307
1634
  }
1308
- const relay = new RelayClient(config, handleMCPRequest, async () => {
1309
- console.log("\n\u{1F511} \uC138\uC158\uC774 \uB9CC\uB8CC\uB410\uC2B5\uB2C8\uB2E4. \uC790\uB3D9\uC73C\uB85C \uC7AC\uC778\uC99D\uD569\uB2C8\uB2E4...");
1310
- try {
1311
- let waitingPrinted = false;
1312
- const authResult = await authenticate(
1313
- deviceName,
1314
- platformName,
1315
- (uri) => {
1316
- console.log(" Opening browser for re-auth...");
1317
- console.log(` \u2192 ${uri}`);
1318
- process.stdout.write(" Waiting for login \xB7");
1319
- },
1320
- () => {
1321
- process.stdout.write("\xB7");
1322
- }
1323
- );
1324
- console.log(`
1325
- \u2705 \uC7AC\uC778\uC99D \uC644\uB8CC`);
1326
- config.token = authResult.token;
1327
- saveConfig(config);
1328
- relay.restart();
1329
- } catch (e) {
1330
- console.error("\n\u274C \uC7AC\uC778\uC99D \uC2E4\uD328:", e);
1331
- process.exit(1);
1332
- }
1333
- });
1334
- await relay.connect();
1335
1635
  console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1336
- console.log(" \u2705 ALL SET \u2014 Your AI can now control this device");
1636
+ console.log(" STEP 5 \xB7 Starting Background Service");
1337
1637
  console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1638
+ const svc = new ServiceManager();
1639
+ try {
1640
+ await svc.install();
1641
+ console.log(" \u25C9 Service registered ........... \u2705");
1642
+ console.log(" \u25C9 Auto-start on boot ........... \u2705");
1643
+ } catch (e) {
1644
+ console.warn(` \u26A0\uFE0F \uC11C\uBE44\uC2A4 \uB4F1\uB85D \uC2E4\uD328: ${e.message}`);
1645
+ console.warn(" \uBC31\uADF8\uB77C\uC6B4\uB4DC \uD504\uB85C\uC138\uC2A4\uB85C\uB9CC \uC2E4\uD589\uD569\uB2C8\uB2E4.");
1646
+ startDaemon(port);
1647
+ }
1338
1648
  const webUrl = process.env.JUNIS_WEB_URL ?? "https://junis.ai";
1339
1649
  console.log("");
1340
- console.log(" Opening your AI team...");
1650
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1651
+ console.log(" \u2705 ALL SET \u2014 Junis\uAC00 \uBC31\uADF8\uB77C\uC6B4\uB4DC\uC5D0\uC11C \uC2E4\uD589 \uC911\uC785\uB2C8\uB2E4.");
1652
+ console.log(" \uBD80\uD305 \uC2DC \uC790\uB3D9\uC73C\uB85C \uC2DC\uC791\uB429\uB2C8\uB2E4.");
1653
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1654
+ console.log("");
1341
1655
  console.log(` \u2192 ${webUrl}`);
1342
- console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
1343
- console.log("\u2502 \u2502");
1344
- console.log("\u2502 Try asking your AI: \u2502");
1345
- console.log("\u2502 \u2502");
1346
- console.log('\u2502 "\uB370\uC2A4\uD06C\uD1B1 \uD30C\uC77C \uBAA9\uB85D \uBCF4\uC5EC\uC918" \u2502');
1347
- console.log('\u2502 "\uD06C\uB86C\uC5D0\uC11C \uC624\uB298 \uB274\uC2A4 \uAC80\uC0C9\uD574\uC918" \u2502');
1348
- console.log('\u2502 "\uCE74\uBA54\uB77C\uB85C \uC0AC\uC9C4 \uD55C \uC7A5 \uCC0D\uC5B4\uC918" \u2502');
1349
- console.log('\u2502 "\uC774 \uC8FC\uD53C\uD130 \uB178\uD2B8\uBD81 3\uBC88 \uC140 \uC218\uC815\uD574\uC918" \u2502');
1350
- console.log("\u2502 \u2502");
1351
- console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
1352
1656
  console.log("");
1353
- console.log(" Device agent running. Press Ctrl+C to disconnect.");
1657
+ console.log(" \uC885\uB8CC\uD558\uB824\uBA74: npx junis stop");
1354
1658
  console.log("");
1659
+ process.exit(0);
1660
+ });
1661
+ import_commander.program.command("stop").description("\uBC31\uADF8\uB77C\uC6B4\uB4DC \uC11C\uBE44\uC2A4 \uC911\uC9C0 \uBC0F \uC790\uB3D9\uC2DC\uC791 \uD574\uC81C").action(async () => {
1662
+ const stopped = stopDaemon();
1663
+ const svc = new ServiceManager();
1664
+ await svc.uninstall();
1665
+ if (stopped) {
1666
+ console.log("\u2705 Junis \uC11C\uBE44\uC2A4\uAC00 \uC911\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
1667
+ } else {
1668
+ console.log("\u2139\uFE0F \uC2E4\uD589 \uC911\uC778 Junis \uD504\uB85C\uC138\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
1669
+ }
1670
+ console.log(" \uC790\uB3D9\uC2DC\uC791\uC774 \uD574\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
1355
1671
  });
1356
1672
  import_commander.program.command("logout").description("\uC778\uC99D \uC815\uBCF4 \uC0AD\uC81C").action(() => {
1357
1673
  clearConfig();
@@ -1359,14 +1675,17 @@ import_commander.program.command("logout").description("\uC778\uC99D \uC815\uBCF
1359
1675
  });
1360
1676
  import_commander.program.command("status").description("\uD604\uC7AC \uC0C1\uD0DC \uD655\uC778").action(() => {
1361
1677
  const config = loadConfig();
1678
+ const { running, pid } = isRunning();
1362
1679
  if (!config) {
1363
1680
  console.log("\u274C \uC778\uC99D \uC5C6\uC74C (npx junis \uC2E4\uD589 \uD544\uC694)");
1681
+ } else if (running) {
1682
+ console.log(`\u2705 \uC2E4\uD589 \uC911 (PID: ${pid})`);
1683
+ console.log(` \uB514\uBC14\uC774\uC2A4: ${config.device_name}`);
1684
+ console.log(` \uB4F1\uB85D\uC77C: ${config.created_at}`);
1364
1685
  } else {
1365
- console.log(
1366
- `\u2705 \uC778\uC99D\uB428
1367
- \uB514\uBC14\uC774\uC2A4: ${config.device_name}
1368
- \uB4F1\uB85D\uC77C: ${config.created_at}`
1369
- );
1686
+ console.log("\u26A0\uFE0F \uC778\uC99D\uB428, \uC11C\uBE44\uC2A4 \uC911\uC9C0 \uC0C1\uD0DC");
1687
+ console.log(` \uB514\uBC14\uC774\uC2A4: ${config.device_name}`);
1688
+ console.log(" \uC2DC\uC791\uD558\uB824\uBA74: npx junis");
1370
1689
  }
1371
1690
  });
1372
1691
  import_commander.program.parse();