junis 0.2.2 → 0.2.4

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.2",
34
+ version: "0.2.4",
35
35
  description: "One-line device control for AI agents",
36
36
  bin: {
37
37
  junis: "dist/cli/index.js"
@@ -91,7 +91,10 @@ function saveConfig(config) {
91
91
  import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true });
92
92
  import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
93
93
  } catch (err) {
94
- throw new Error(`\uC778\uC99D \uC815\uBCF4 \uC800\uC7A5 \uC2E4\uD328: ${err.message}`);
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)}`);
95
98
  }
96
99
  }
97
100
  function clearConfig() {
@@ -117,11 +120,19 @@ var JUNIS_WEB = (() => {
117
120
  return null;
118
121
  })();
119
122
  async function authenticate(deviceName, platform2, onBrowserOpen, onWaiting) {
120
- const startRes = await fetch(`${JUNIS_API}/api/auth/device/start`, {
121
- method: "POST",
122
- headers: { "Content-Type": "application/json" },
123
- body: JSON.stringify({ device_name: deviceName, platform: platform2 })
124
- });
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
+ }
125
136
  if (!startRes.ok) {
126
137
  const body = await startRes.text().catch(() => "");
127
138
  throw new Error(`Auth \uC2DC\uC791 \uC2E4\uD328: ${startRes.status} ${body}`);
@@ -129,15 +140,29 @@ async function authenticate(deviceName, platform2, onBrowserOpen, onWaiting) {
129
140
  const startData = await startRes.json();
130
141
  const verificationUri = JUNIS_WEB ? startData.verification_uri.replace(/^https?:\/\/[^/]+/, JUNIS_WEB) : startData.verification_uri;
131
142
  onBrowserOpen?.(verificationUri);
132
- 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
+ }
133
152
  const deadline = Date.now() + startData.expires_in * 1e3;
134
153
  const intervalMs = startData.interval * 1e3;
135
154
  let waitingCallbackCalled = false;
136
155
  while (Date.now() < deadline) {
137
156
  await sleep(intervalMs);
138
- const pollRes = await fetch(
139
- `${JUNIS_API}/api/auth/device/poll?code=${startData.device_code}`
140
- );
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
+ }
141
166
  if (pollRes.status === 202) {
142
167
  if (!waitingCallbackCalled) {
143
168
  waitingCallbackCalled = true;
@@ -188,15 +213,18 @@ var RelayClient = class {
188
213
  if (this.destroyed) return;
189
214
  const url = `${JUNIS_WS}/ws/devices/${this.config.device_key}`;
190
215
  console.log(`\u{1F517} \uB9B4\uB808\uC774 \uC11C\uBC84 \uC5F0\uACB0 \uC911...`);
191
- this.ws = new import_ws.default(url, {
216
+ const ws = new import_ws.default(url, {
192
217
  headers: { Authorization: `Bearer ${this.config.token}` }
193
218
  });
194
- this.ws.on("open", () => {
219
+ this.ws = ws;
220
+ ws.on("open", () => {
221
+ if (this.ws !== ws) return;
195
222
  console.log("\u2705 \uB9B4\uB808\uC774 \uC11C\uBC84 \uC5F0\uACB0\uB428");
196
223
  this.reconnectDelay = 1e3;
197
224
  this.startHeartbeat();
198
225
  });
199
- this.ws.on("message", async (raw) => {
226
+ ws.on("message", async (raw) => {
227
+ if (this.ws !== ws) return;
200
228
  try {
201
229
  const msg = JSON.parse(raw.toString());
202
230
  if (msg.type === "pong") return;
@@ -215,9 +243,10 @@ var RelayClient = class {
215
243
  } catch {
216
244
  }
217
245
  });
218
- this.ws.on("close", async (code) => {
219
- this.stopHeartbeat();
246
+ ws.on("close", async (code) => {
220
247
  if (this.destroyed) return;
248
+ if (this.ws !== ws) return;
249
+ this.stopHeartbeat();
221
250
  if (code === 4001) {
222
251
  this.destroyed = true;
223
252
  if (this.onAuthExpired) {
@@ -234,7 +263,7 @@ var RelayClient = class {
234
263
  setTimeout(() => this.connect(), this.reconnectDelay);
235
264
  this.reconnectDelay = Math.min(this.reconnectDelay * 2, 3e4);
236
265
  });
237
- this.ws.on("error", (err) => {
266
+ ws.on("error", (err) => {
238
267
  console.error(`\uB9B4\uB808\uC774 \uC624\uB958: ${err.message}`);
239
268
  });
240
269
  }
@@ -326,8 +355,16 @@ ${error.stderr ?? ""}`
326
355
  encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("\uC778\uCF54\uB529")
327
356
  },
328
357
  async ({ path: filePath, encoding }) => {
329
- const content = await import_promises.default.readFile(filePath, encoding);
330
- 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
+ }
331
368
  }
332
369
  );
333
370
  server.tool(
@@ -350,9 +387,17 @@ ${error.stderr ?? ""}`
350
387
  path: import_zod.z.string().describe("\uB514\uB809\uD1A0\uB9AC \uACBD\uB85C")
351
388
  },
352
389
  async ({ path: dirPath }) => {
353
- const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true });
354
- const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
355
- 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
+ }
356
401
  }
357
402
  );
358
403
  server.tool(
@@ -372,12 +417,13 @@ ${error.stderr ?? ""}`
372
417
  );
373
418
  return { content: [{ type: "text", text: stdout || "\uACB0\uACFC \uC5C6\uC74C" }] };
374
419
  } catch {
375
- 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 });
376
422
  const results = [];
377
423
  for (const file of files.slice(0, 100)) {
378
424
  try {
379
425
  const content = await import_promises.default.readFile(
380
- import_path2.default.join(directory, file),
426
+ import_path2.default.join(safeDirectory, file),
381
427
  "utf-8"
382
428
  );
383
429
  const lines = content.split("\n");
@@ -576,14 +622,14 @@ var BrowserTools = class {
576
622
  path: import_zod2.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (\uC5C6\uC73C\uBA74 base64 \uBC18\uD658)"),
577
623
  full_page: import_zod2.z.boolean().optional().default(false)
578
624
  },
579
- ({ path: path3, full_page }) => this.withLock(async () => {
625
+ ({ path: path4, full_page }) => this.withLock(async () => {
580
626
  const page = requirePage();
581
627
  const screenshot = await page.screenshot({
582
- path: path3 ?? void 0,
628
+ path: path4 ?? void 0,
583
629
  fullPage: full_page
584
630
  });
585
- if (path3) {
586
- return { content: [{ type: "text", text: `\uC800\uC7A5 \uC644\uB8CC: ${path3}` }] };
631
+ if (path4) {
632
+ return { content: [{ type: "text", text: `\uC800\uC7A5 \uC644\uB8CC: ${path4}` }] };
587
633
  }
588
634
  return {
589
635
  content: [
@@ -615,21 +661,28 @@ var BrowserTools = class {
615
661
  "JavaScript \uC2E4\uD589",
616
662
  { code: import_zod2.z.string().describe("\uC2E4\uD589\uD560 JavaScript \uCF54\uB4DC") },
617
663
  ({ code }) => this.withLock(async () => {
618
- const result = await requirePage().evaluate(code);
619
- return {
620
- content: [
621
- { type: "text", text: JSON.stringify(result, null, 2) }
622
- ]
623
- };
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
+ }
624
677
  })
625
678
  );
626
679
  server.tool(
627
680
  "browser_pdf",
628
681
  "\uD604\uC7AC \uD398\uC774\uC9C0 PDF \uC800\uC7A5",
629
682
  { path: import_zod2.z.string().describe("\uC800\uC7A5 \uACBD\uB85C (.pdf)") },
630
- ({ path: path3 }) => this.withLock(async () => {
631
- await requirePage().pdf({ path: path3 });
632
- return { content: [{ type: "text", text: `PDF \uC800\uC7A5 \uC644\uB8CC: ${path3}` }] };
683
+ ({ path: path4 }) => this.withLock(async () => {
684
+ await requirePage().pdf({ path: path4 });
685
+ return { content: [{ type: "text", text: `PDF \uC800\uC7A5 \uC644\uB8CC: ${path4}` }] };
633
686
  })
634
687
  );
635
688
  }
@@ -646,7 +699,7 @@ async function readNotebook(filePath) {
646
699
  try {
647
700
  return JSON.parse(raw);
648
701
  } catch {
649
- throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 .ipynb \uD30C\uC77C: ${filePath}`);
702
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 Jupyter \uB178\uD2B8\uBD81 \uD30C\uC77C\uC785\uB2C8\uB2E4: ${filePath}`);
650
703
  }
651
704
  }
652
705
  async function writeNotebook(filePath, nb) {
@@ -742,16 +795,21 @@ var NotebookTools = class {
742
795
  execution_count: cellType === "code" ? null : void 0
743
796
  };
744
797
  let actualIndex;
798
+ let warning = "";
745
799
  if (position === void 0 || position === null) {
746
800
  nb.cells.push(newCell);
747
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)`;
748
806
  } else {
749
- const clamped = Math.max(0, Math.min(position, nb.cells.length));
807
+ const clamped = Math.max(0, position);
750
808
  nb.cells.splice(clamped, 0, newCell);
751
809
  actualIndex = clamped;
752
810
  }
753
811
  await writeNotebook(filePath, nb);
754
- 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}` }] };
755
813
  }
756
814
  );
757
815
  server.tool(
@@ -809,9 +867,11 @@ var DeviceTools = class {
809
867
  }
810
868
  const { readFileSync, unlinkSync } = await import("fs");
811
869
  const data = readFileSync(tmpPath).toString("base64");
812
- if (isTmp) try {
813
- unlinkSync(tmpPath);
814
- } catch {
870
+ if (isTmp) {
871
+ try {
872
+ unlinkSync(tmpPath);
873
+ } catch {
874
+ }
815
875
  }
816
876
  return {
817
877
  content: [{ type: "image", data, mimeType: "image/png" }]
@@ -836,7 +896,14 @@ var DeviceTools = class {
836
896
  try {
837
897
  await execAsync3(cmd);
838
898
  } catch (err) {
839
- throw new Error(`\uCE74\uBA54\uB77C \uCD2C\uC601 \uC2E4\uD328: ${err.message}`);
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
+ };
840
907
  }
841
908
  const { readFileSync, unlinkSync } = await import("fs");
842
909
  const data = readFileSync(tmpPath).toString("base64");
@@ -858,11 +925,17 @@ var DeviceTools = class {
858
925
  },
859
926
  async ({ title, message }) => {
860
927
  const p = platform();
861
- const cmd = {
862
- mac: `osascript -e 'display notification "${message}" with title "${title}"'`,
863
- 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())"`,
864
- linux: `notify-send "${title}" "${message}"`
865
- }[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
+ }
866
939
  await execAsync3(cmd);
867
940
  return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
868
941
  }
@@ -907,9 +980,9 @@ var DeviceTools = class {
907
980
  return { content: [{ type: "text", text: "\uC774\uBBF8 \uB179\uD654 \uC911\uC785\uB2C8\uB2E4." }] };
908
981
  }
909
982
  const tmpPath = output_path ?? `/tmp/junis_record_${Date.now()}.mp4`;
910
- const { spawn } = await import("child_process");
983
+ const { spawn: spawn2 } = await import("child_process");
911
984
  const cmd = p === "mac" ? ["screencapture", ["-v", tmpPath]] : ["ffmpeg", ["-f", p === "win" ? "gdigrab" : "x11grab", "-i", p === "win" ? "desktop" : ":0.0", tmpPath]];
912
- const child = spawn(cmd[0], cmd[1], { detached: true, stdio: "ignore" });
985
+ const child = spawn2(cmd[0], cmd[1], { detached: true, stdio: "ignore" });
913
986
  child.unref();
914
987
  screenRecordPid = child.pid ?? null;
915
988
  return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${screenRecordPid})` }] };
@@ -919,6 +992,7 @@ var DeviceTools = class {
919
992
  }
920
993
  try {
921
994
  process.kill(screenRecordPid, "SIGINT");
995
+ await new Promise((r) => setTimeout(r, 1e3));
922
996
  } catch {
923
997
  }
924
998
  screenRecordPid = null;
@@ -1221,6 +1295,182 @@ async function handleMCPRequest(id, payload) {
1221
1295
  return null;
1222
1296
  }
1223
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
+ env: { ...process.env }
1344
+ // JUNIS_API_URL, JUNIS_WS_URL 등 현재 env 상속
1345
+ });
1346
+ child.unref();
1347
+ const pid = child.pid;
1348
+ writePid(pid);
1349
+ }
1350
+ function stopDaemon() {
1351
+ const { running, pid } = isRunning();
1352
+ if (!running || !pid) return false;
1353
+ try {
1354
+ process.kill(pid, "SIGTERM");
1355
+ try {
1356
+ import_fs2.default.unlinkSync(PID_FILE);
1357
+ } catch {
1358
+ }
1359
+ return true;
1360
+ } catch {
1361
+ return false;
1362
+ }
1363
+ }
1364
+ var ServiceManager = class {
1365
+ get platform() {
1366
+ if (process.platform === "darwin") return "mac";
1367
+ if (process.platform === "win32") return "win";
1368
+ return "linux";
1369
+ }
1370
+ async install() {
1371
+ const nodePath = process.execPath;
1372
+ const scriptPath = process.argv[1];
1373
+ if (this.platform === "mac") {
1374
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
1375
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1376
+ <plist version="1.0">
1377
+ <dict>
1378
+ <key>Label</key>
1379
+ <string>ai.junis</string>
1380
+ <key>ProgramArguments</key>
1381
+ <array>
1382
+ <string>${nodePath}</string>
1383
+ <string>${scriptPath}</string>
1384
+ <string>start</string>
1385
+ <string>--daemon</string>
1386
+ </array>
1387
+ <key>EnvironmentVariables</key>
1388
+ <dict>
1389
+ <key>HOME</key>
1390
+ <string>${import_os2.default.homedir()}</string>
1391
+ <key>PATH</key>
1392
+ <string>${process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin"}</string>
1393
+ ${process.env.JUNIS_API_URL ? `<key>JUNIS_API_URL</key>
1394
+ <string>${process.env.JUNIS_API_URL}</string>` : ""}
1395
+ ${process.env.JUNIS_WS_URL ? `<key>JUNIS_WS_URL</key>
1396
+ <string>${process.env.JUNIS_WS_URL}</string>` : ""}
1397
+ ${process.env.JUNIS_WEB_URL ? `<key>JUNIS_WEB_URL</key>
1398
+ <string>${process.env.JUNIS_WEB_URL}</string>` : ""}
1399
+ </dict>
1400
+ <key>RunAtLoad</key>
1401
+ <true/>
1402
+ <key>KeepAlive</key>
1403
+ <true/>
1404
+ <key>StandardOutPath</key>
1405
+ <string>${LOG_FILE}</string>
1406
+ <key>StandardErrorPath</key>
1407
+ <string>${LOG_FILE}</string>
1408
+ </dict>
1409
+ </plist>`;
1410
+ import_fs2.default.mkdirSync(import_path3.default.dirname(PLIST_PATH), { recursive: true });
1411
+ import_fs2.default.mkdirSync(LOG_DIR, { recursive: true });
1412
+ import_fs2.default.writeFileSync(PLIST_PATH, plist, "utf-8");
1413
+ try {
1414
+ (0, import_child_process4.execSync)(`launchctl unload "${PLIST_PATH}" 2>/dev/null || true`);
1415
+ (0, import_child_process4.execSync)(`launchctl load "${PLIST_PATH}"`);
1416
+ } catch (e) {
1417
+ throw new Error(`launchctl load \uC2E4\uD328: ${e.message}`);
1418
+ }
1419
+ } else if (this.platform === "linux") {
1420
+ const unit = `[Unit]
1421
+ Description=Junis Device Agent
1422
+ After=network.target
1423
+
1424
+ [Service]
1425
+ ExecStart=${nodePath} ${scriptPath} start --daemon
1426
+ Restart=always
1427
+ RestartSec=5
1428
+ Environment=HOME=${import_os2.default.homedir()}
1429
+ Environment=PATH=${process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin"}
1430
+ ${process.env.JUNIS_API_URL ? `Environment=JUNIS_API_URL=${process.env.JUNIS_API_URL}` : ""}
1431
+ ${process.env.JUNIS_WS_URL ? `Environment=JUNIS_WS_URL=${process.env.JUNIS_WS_URL}` : ""}
1432
+ ${process.env.JUNIS_WEB_URL ? `Environment=JUNIS_WEB_URL=${process.env.JUNIS_WEB_URL}` : ""}
1433
+ StandardOutput=append:${LOG_FILE}
1434
+ StandardError=append:${LOG_FILE}
1435
+
1436
+ [Install]
1437
+ WantedBy=default.target`;
1438
+ import_fs2.default.mkdirSync(import_path3.default.dirname(SYSTEMD_PATH), { recursive: true });
1439
+ import_fs2.default.mkdirSync(LOG_DIR, { recursive: true });
1440
+ import_fs2.default.writeFileSync(SYSTEMD_PATH, unit, "utf-8");
1441
+ (0, import_child_process4.execSync)("systemctl --user daemon-reload");
1442
+ (0, import_child_process4.execSync)("systemctl --user enable junis");
1443
+ (0, import_child_process4.execSync)("systemctl --user start junis");
1444
+ } else {
1445
+ (0, import_child_process4.execSync)(
1446
+ `schtasks /Create /F /TN "Junis" /TR "${nodePath} ${scriptPath} start --daemon" /SC ONLOGON /RL HIGHEST`
1447
+ );
1448
+ }
1449
+ }
1450
+ async uninstall() {
1451
+ if (this.platform === "mac") {
1452
+ try {
1453
+ (0, import_child_process4.execSync)(`launchctl unload "${PLIST_PATH}" 2>/dev/null || true`);
1454
+ if (import_fs2.default.existsSync(PLIST_PATH)) import_fs2.default.unlinkSync(PLIST_PATH);
1455
+ } catch {
1456
+ }
1457
+ } else if (this.platform === "linux") {
1458
+ try {
1459
+ (0, import_child_process4.execSync)("systemctl --user stop junis 2>/dev/null || true");
1460
+ (0, import_child_process4.execSync)("systemctl --user disable junis 2>/dev/null || true");
1461
+ if (import_fs2.default.existsSync(SYSTEMD_PATH)) import_fs2.default.unlinkSync(SYSTEMD_PATH);
1462
+ (0, import_child_process4.execSync)("systemctl --user daemon-reload 2>/dev/null || true");
1463
+ } catch {
1464
+ }
1465
+ } else {
1466
+ try {
1467
+ (0, import_child_process4.execSync)('schtasks /Delete /F /TN "Junis" 2>nul || true');
1468
+ } catch {
1469
+ }
1470
+ }
1471
+ }
1472
+ };
1473
+
1224
1474
  // src/cli/index.ts
1225
1475
  var { version } = require_package();
1226
1476
  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);
@@ -1228,9 +1478,9 @@ function getSystemInfo() {
1228
1478
  const platform2 = process.platform;
1229
1479
  if (platform2 === "darwin") {
1230
1480
  try {
1231
- const { execSync } = require("child_process");
1232
- const sw = execSync("sw_vers -productVersion", { encoding: "utf8" }).trim();
1233
- const hw = execSync("sysctl -n machdep.cpu.brand_string", { encoding: "utf8" }).trim();
1481
+ const { execSync: execSync2 } = require("child_process");
1482
+ const sw = execSync2("sw_vers -productVersion", { encoding: "utf8" }).trim();
1483
+ const hw = execSync2("sysctl -n machdep.cpu.brand_string", { encoding: "utf8" }).trim();
1234
1484
  return `macOS ${sw} (${hw})`;
1235
1485
  } catch {
1236
1486
  return "macOS";
@@ -1272,22 +1522,68 @@ function printStep1(port) {
1272
1522
  console.log(` \u25C9 Local MCP endpoint ........... http://localhost:${port}/mcp`);
1273
1523
  console.log("");
1274
1524
  }
1275
- 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) => {
1525
+ 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) => {
1276
1526
  const port = parseInt(options.port, 10);
1527
+ if (options.daemon) {
1528
+ writePid(process.pid);
1529
+ if (options.local) {
1530
+ await startMCPServer(port);
1531
+ return;
1532
+ }
1533
+ let config2 = options.reset ? null : loadConfig();
1534
+ if (!config2) {
1535
+ console.error("\u274C \uC778\uC99D \uC815\uBCF4 \uC5C6\uC74C. npx junis \uB97C \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
1536
+ process.exit(1);
1537
+ }
1538
+ const deviceName2 = config2.device_name;
1539
+ const platformName2 = process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux";
1540
+ const actualPort = await startMCPServer(port);
1541
+ console.log(`[junis daemon] MCP server started on port ${actualPort}`);
1542
+ const relay = new RelayClient(config2, handleMCPRequest, async () => {
1543
+ console.log("[junis daemon] \uC138\uC158 \uB9CC\uB8CC - \uC7AC\uC778\uC99D \uD544\uC694");
1544
+ try {
1545
+ let waitingPrinted = false;
1546
+ const authResult = await authenticate(
1547
+ deviceName2,
1548
+ platformName2,
1549
+ (uri) => {
1550
+ console.log(`[junis daemon] \uBE0C\uB77C\uC6B0\uC800 \uC7AC\uC778\uC99D: ${uri}`);
1551
+ },
1552
+ () => {
1553
+ if (!waitingPrinted) waitingPrinted = true;
1554
+ }
1555
+ );
1556
+ config2.token = authResult.token;
1557
+ saveConfig(config2);
1558
+ relay.restart();
1559
+ } catch (e) {
1560
+ console.error("[junis daemon] \uC7AC\uC778\uC99D \uC2E4\uD328:", e);
1561
+ process.exit(1);
1562
+ }
1563
+ });
1564
+ await relay.connect();
1565
+ console.log("[junis daemon] relay connected");
1566
+ return;
1567
+ }
1277
1568
  printBanner();
1278
1569
  if (options.local) {
1279
- const actualPort2 = await startMCPServer(port);
1280
- printStep1(actualPort2);
1570
+ const actualPort = await startMCPServer(port);
1571
+ printStep1(actualPort);
1281
1572
  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");
1282
1573
  console.log(" \u2705 ALL SET \u2014 Local MCP server is running");
1283
1574
  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");
1284
1575
  return;
1285
1576
  }
1577
+ const { running, pid } = isRunning();
1578
+ if (running) {
1579
+ console.log(`\u2705 Junis \uC2E4\uD589 \uC911\uC785\uB2C8\uB2E4. (PID: ${pid})`);
1580
+ console.log(" \uC885\uB8CC\uD558\uB824\uBA74: npx junis stop");
1581
+ return;
1582
+ }
1286
1583
  let config = options.reset ? null : loadConfig();
1287
1584
  const deviceName = config?.device_name ?? `${process.env["USER"] ?? "user"}'s ${getDeviceName()}`;
1288
1585
  const platformName = process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux";
1289
- const actualPort = await startMCPServer(port);
1290
- printStep1(actualPort);
1586
+ printStep1(port);
1291
1587
  if (!config) {
1292
1588
  let waitingPrinted = false;
1293
1589
  const authResult = await authenticate(
@@ -1348,53 +1644,47 @@ import_commander.program.command("start", { isDefault: true }).description("Juni
1348
1644
  console.log(" \u25C9 Status ....................... \u{1F7E2} online");
1349
1645
  console.log("");
1350
1646
  }
1351
- const relay = new RelayClient(config, handleMCPRequest, async () => {
1352
- console.log("\n\u{1F511} \uC138\uC158\uC774 \uB9CC\uB8CC\uB410\uC2B5\uB2C8\uB2E4. \uC790\uB3D9\uC73C\uB85C \uC7AC\uC778\uC99D\uD569\uB2C8\uB2E4...");
1353
- try {
1354
- let waitingPrinted = false;
1355
- const authResult = await authenticate(
1356
- deviceName,
1357
- platformName,
1358
- (uri) => {
1359
- console.log(" Opening browser for re-auth...");
1360
- console.log(` \u2192 ${uri}`);
1361
- process.stdout.write(" Waiting for login \xB7");
1362
- },
1363
- () => {
1364
- process.stdout.write("\xB7");
1365
- }
1366
- );
1367
- console.log(`
1368
- \u2705 \uC7AC\uC778\uC99D \uC644\uB8CC`);
1369
- config.token = authResult.token;
1370
- saveConfig(config);
1371
- relay.restart();
1372
- } catch (e) {
1373
- console.error("\n\u274C \uC7AC\uC778\uC99D \uC2E4\uD328:", e);
1374
- process.exit(1);
1375
- }
1376
- });
1377
- await relay.connect();
1378
1647
  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");
1379
- console.log(" \u2705 ALL SET \u2014 Your AI can now control this device");
1648
+ console.log(" STEP 5 \xB7 Starting Background Service");
1380
1649
  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");
1650
+ const svc = new ServiceManager();
1651
+ try {
1652
+ await svc.install();
1653
+ console.log(" \u25C9 Service registered ........... \u2705");
1654
+ console.log(" \u25C9 Auto-start on boot ........... \u2705");
1655
+ } catch (e) {
1656
+ console.warn(` \u26A0\uFE0F \uC11C\uBE44\uC2A4 \uB4F1\uB85D \uC2E4\uD328: ${e.message}`);
1657
+ console.warn(" \uBC31\uADF8\uB77C\uC6B4\uB4DC \uD504\uB85C\uC138\uC2A4\uB85C\uB9CC \uC2E4\uD589\uD569\uB2C8\uB2E4.");
1658
+ startDaemon(port);
1659
+ }
1381
1660
  const webUrl = process.env.JUNIS_WEB_URL ?? "https://junis.ai";
1382
1661
  console.log("");
1383
- console.log(" Opening your AI team...");
1662
+ 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");
1663
+ console.log(" \u2705 ALL SET \u2014 Junis\uAC00 \uBC31\uADF8\uB77C\uC6B4\uB4DC\uC5D0\uC11C \uC2E4\uD589 \uC911\uC785\uB2C8\uB2E4.");
1664
+ console.log(" \uBD80\uD305 \uC2DC \uC790\uB3D9\uC73C\uB85C \uC2DC\uC791\uB429\uB2C8\uB2E4.");
1665
+ 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");
1666
+ console.log("");
1384
1667
  console.log(` \u2192 ${webUrl}`);
1385
- 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");
1386
- console.log("\u2502 \u2502");
1387
- console.log("\u2502 Try asking your AI: \u2502");
1388
- console.log("\u2502 \u2502");
1389
- console.log('\u2502 "\uB370\uC2A4\uD06C\uD1B1 \uD30C\uC77C \uBAA9\uB85D \uBCF4\uC5EC\uC918" \u2502');
1390
- console.log('\u2502 "\uD06C\uB86C\uC5D0\uC11C \uC624\uB298 \uB274\uC2A4 \uAC80\uC0C9\uD574\uC918" \u2502');
1391
- console.log('\u2502 "\uCE74\uBA54\uB77C\uB85C \uC0AC\uC9C4 \uD55C \uC7A5 \uCC0D\uC5B4\uC918" \u2502');
1392
- console.log('\u2502 "\uC774 \uC8FC\uD53C\uD130 \uB178\uD2B8\uBD81 3\uBC88 \uC140 \uC218\uC815\uD574\uC918" \u2502');
1393
- console.log("\u2502 \u2502");
1394
- 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");
1395
1668
  console.log("");
1396
- console.log(" Device agent running. Press Ctrl+C to disconnect.");
1669
+ console.log(" \uC885\uB8CC\uD558\uB824\uBA74: npx junis stop");
1397
1670
  console.log("");
1671
+ process.exit(0);
1672
+ });
1673
+ 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 () => {
1674
+ const stopped = stopDaemon();
1675
+ const svc = new ServiceManager();
1676
+ let serviceUninstalled = false;
1677
+ try {
1678
+ await svc.uninstall();
1679
+ serviceUninstalled = true;
1680
+ } catch {
1681
+ }
1682
+ if (stopped || serviceUninstalled) {
1683
+ console.log("\u2705 Junis \uC11C\uBE44\uC2A4\uAC00 \uC911\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
1684
+ console.log(" \uC790\uB3D9\uC2DC\uC791\uC774 \uD574\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
1685
+ } else {
1686
+ console.log("\u2139\uFE0F \uC2E4\uD589 \uC911\uC778 Junis \uD504\uB85C\uC138\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
1687
+ }
1398
1688
  });
1399
1689
  import_commander.program.command("logout").description("\uC778\uC99D \uC815\uBCF4 \uC0AD\uC81C").action(() => {
1400
1690
  clearConfig();
@@ -1402,14 +1692,17 @@ import_commander.program.command("logout").description("\uC778\uC99D \uC815\uBCF
1402
1692
  });
1403
1693
  import_commander.program.command("status").description("\uD604\uC7AC \uC0C1\uD0DC \uD655\uC778").action(() => {
1404
1694
  const config = loadConfig();
1695
+ const { running, pid } = isRunning();
1405
1696
  if (!config) {
1406
1697
  console.log("\u274C \uC778\uC99D \uC5C6\uC74C (npx junis \uC2E4\uD589 \uD544\uC694)");
1698
+ } else if (running) {
1699
+ console.log(`\u2705 \uC2E4\uD589 \uC911 (PID: ${pid})`);
1700
+ console.log(` \uB514\uBC14\uC774\uC2A4: ${config.device_name}`);
1701
+ console.log(` \uB4F1\uB85D\uC77C: ${config.created_at}`);
1407
1702
  } else {
1408
- console.log(
1409
- `\u2705 \uC778\uC99D\uB428
1410
- \uB514\uBC14\uC774\uC2A4: ${config.device_name}
1411
- \uB4F1\uB85D\uC77C: ${config.created_at}`
1412
- );
1703
+ console.log("\u26A0\uFE0F \uC778\uC99D\uB428, \uC11C\uBE44\uC2A4 \uC911\uC9C0 \uC0C1\uD0DC");
1704
+ console.log(` \uB514\uBC14\uC774\uC2A4: ${config.device_name}`);
1705
+ console.log(" \uC2DC\uC791\uD558\uB824\uBA74: npx junis");
1413
1706
  }
1414
1707
  });
1415
1708
  import_commander.program.parse();
@@ -92,8 +92,16 @@ ${error.stderr ?? ""}`
92
92
  encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("\uC778\uCF54\uB529")
93
93
  },
94
94
  async ({ path: filePath, encoding }) => {
95
- const content = await import_promises.default.readFile(filePath, encoding);
96
- return { content: [{ type: "text", text: content }] };
95
+ try {
96
+ const content = await import_promises.default.readFile(filePath, encoding);
97
+ return { content: [{ type: "text", text: content }] };
98
+ } catch (err) {
99
+ const e = err;
100
+ if (e.code === "ENOENT") {
101
+ return { content: [{ type: "text", text: `\u274C \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath}` }], isError: true };
102
+ }
103
+ return { content: [{ type: "text", text: `\u274C \uD30C\uC77C \uC77D\uAE30 \uC2E4\uD328: ${e.message}` }], isError: true };
104
+ }
97
105
  }
98
106
  );
99
107
  server.tool(
@@ -116,9 +124,17 @@ ${error.stderr ?? ""}`
116
124
  path: import_zod.z.string().describe("\uB514\uB809\uD1A0\uB9AC \uACBD\uB85C")
117
125
  },
118
126
  async ({ path: dirPath }) => {
119
- const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true });
120
- const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
121
- return { content: [{ type: "text", text: lines.join("\n") }] };
127
+ try {
128
+ const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true });
129
+ const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
130
+ return { content: [{ type: "text", text: lines.join("\n") }] };
131
+ } catch (err) {
132
+ const e = err;
133
+ if (e.code === "ENOENT") {
134
+ return { content: [{ type: "text", text: `\u274C \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${dirPath}` }], isError: true };
135
+ }
136
+ return { content: [{ type: "text", text: `\u274C \uB514\uB809\uD1A0\uB9AC \uC77D\uAE30 \uC2E4\uD328: ${e.message}` }], isError: true };
137
+ }
122
138
  }
123
139
  );
124
140
  server.tool(
@@ -138,12 +154,13 @@ ${error.stderr ?? ""}`
138
154
  );
139
155
  return { content: [{ type: "text", text: stdout || "\uACB0\uACFC \uC5C6\uC74C" }] };
140
156
  } catch {
141
- const files = await (0, import_glob.glob)(file_pattern, { cwd: directory });
157
+ const safeDirectory = import_path.default.resolve(directory);
158
+ const files = await (0, import_glob.glob)(file_pattern, { cwd: safeDirectory });
142
159
  const results = [];
143
160
  for (const file of files.slice(0, 100)) {
144
161
  try {
145
162
  const content = await import_promises.default.readFile(
146
- import_path.default.join(directory, file),
163
+ import_path.default.join(safeDirectory, file),
147
164
  "utf-8"
148
165
  );
149
166
  const lines = content.split("\n");
@@ -381,12 +398,19 @@ var BrowserTools = class {
381
398
  "JavaScript \uC2E4\uD589",
382
399
  { code: import_zod2.z.string().describe("\uC2E4\uD589\uD560 JavaScript \uCF54\uB4DC") },
383
400
  ({ code }) => this.withLock(async () => {
384
- const result = await requirePage().evaluate(code);
385
- return {
386
- content: [
387
- { type: "text", text: JSON.stringify(result, null, 2) }
388
- ]
389
- };
401
+ try {
402
+ const result = await requirePage().evaluate(code);
403
+ return {
404
+ content: [
405
+ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }
406
+ ]
407
+ };
408
+ } catch (err) {
409
+ return {
410
+ content: [{ type: "text", text: `\u274C JavaScript \uC2E4\uD589 \uC624\uB958: ${err.message}` }],
411
+ isError: true
412
+ };
413
+ }
390
414
  })
391
415
  );
392
416
  server.tool(
@@ -412,7 +436,7 @@ async function readNotebook(filePath) {
412
436
  try {
413
437
  return JSON.parse(raw);
414
438
  } catch {
415
- throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 .ipynb \uD30C\uC77C: ${filePath}`);
439
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 Jupyter \uB178\uD2B8\uBD81 \uD30C\uC77C\uC785\uB2C8\uB2E4: ${filePath}`);
416
440
  }
417
441
  }
418
442
  async function writeNotebook(filePath, nb) {
@@ -508,16 +532,21 @@ var NotebookTools = class {
508
532
  execution_count: cellType === "code" ? null : void 0
509
533
  };
510
534
  let actualIndex;
535
+ let warning = "";
511
536
  if (position === void 0 || position === null) {
512
537
  nb.cells.push(newCell);
513
538
  actualIndex = nb.cells.length - 1;
539
+ } else if (position > nb.cells.length) {
540
+ nb.cells.push(newCell);
541
+ actualIndex = nb.cells.length - 1;
542
+ warning = ` (\uACBD\uACE0: position ${position}\uC774 \uBC94\uC704\uB97C \uCD08\uACFC\uD558\uC5EC \uB05D(index: ${actualIndex})\uC5D0 \uCD94\uAC00\uB428)`;
514
543
  } else {
515
- const clamped = Math.max(0, Math.min(position, nb.cells.length));
544
+ const clamped = Math.max(0, position);
516
545
  nb.cells.splice(clamped, 0, newCell);
517
546
  actualIndex = clamped;
518
547
  }
519
548
  await writeNotebook(filePath, nb);
520
- return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})` }] };
549
+ return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})${warning}` }] };
521
550
  }
522
551
  );
523
552
  server.tool(
@@ -575,9 +604,11 @@ var DeviceTools = class {
575
604
  }
576
605
  const { readFileSync, unlinkSync } = await import("fs");
577
606
  const data = readFileSync(tmpPath).toString("base64");
578
- if (isTmp) try {
579
- unlinkSync(tmpPath);
580
- } catch {
607
+ if (isTmp) {
608
+ try {
609
+ unlinkSync(tmpPath);
610
+ } catch {
611
+ }
581
612
  }
582
613
  return {
583
614
  content: [{ type: "image", data, mimeType: "image/png" }]
@@ -602,7 +633,14 @@ var DeviceTools = class {
602
633
  try {
603
634
  await execAsync3(cmd);
604
635
  } catch (err) {
605
- throw new Error(`\uCE74\uBA54\uB77C \uCD2C\uC601 \uC2E4\uD328: ${err.message}`);
636
+ const e = err;
637
+ return {
638
+ content: [{ type: "text", text: `\u274C \uCE74\uBA54\uB77C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uAC70\uB098 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
639
+ \uC6D0\uC778: ${e.message}
640
+
641
+ \uCE74\uBA54\uB77C\uAC00 \uC5F0\uACB0\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.` }],
642
+ isError: true
643
+ };
606
644
  }
607
645
  const { readFileSync, unlinkSync } = await import("fs");
608
646
  const data = readFileSync(tmpPath).toString("base64");
@@ -624,11 +662,17 @@ var DeviceTools = class {
624
662
  },
625
663
  async ({ title, message }) => {
626
664
  const p = platform();
627
- const cmd = {
628
- mac: `osascript -e 'display notification "${message}" with title "${title}"'`,
629
- 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())"`,
630
- linux: `notify-send "${title}" "${message}"`
631
- }[p];
665
+ let cmd;
666
+ if (p === "win") {
667
+ const script = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${message.replace(/'/g, "''")}', '${title.replace(/'/g, "''")}')`;
668
+ const encoded = Buffer.from(script, "utf16le").toString("base64");
669
+ cmd = `powershell -NoProfile -EncodedCommand ${encoded}`;
670
+ } else {
671
+ cmd = {
672
+ mac: `osascript -e 'display notification "${message.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"'`,
673
+ linux: `notify-send "${title.replace(/"/g, '\\"')}" "${message.replace(/"/g, '\\"')}"`
674
+ }[p] ?? "";
675
+ }
632
676
  await execAsync3(cmd);
633
677
  return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
634
678
  }
@@ -685,6 +729,7 @@ var DeviceTools = class {
685
729
  }
686
730
  try {
687
731
  process.kill(screenRecordPid, "SIGINT");
732
+ await new Promise((r) => setTimeout(r, 1e3));
688
733
  } catch {
689
734
  }
690
735
  screenRecordPid = null;
@@ -81,8 +81,16 @@ ${error.stderr ?? ""}`
81
81
  encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("\uC778\uCF54\uB529")
82
82
  },
83
83
  async ({ path: filePath, encoding }) => {
84
- const content = await import_promises.default.readFile(filePath, encoding);
85
- return { content: [{ type: "text", text: content }] };
84
+ try {
85
+ const content = await import_promises.default.readFile(filePath, encoding);
86
+ return { content: [{ type: "text", text: content }] };
87
+ } catch (err) {
88
+ const e = err;
89
+ if (e.code === "ENOENT") {
90
+ return { content: [{ type: "text", text: `\u274C \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath}` }], isError: true };
91
+ }
92
+ return { content: [{ type: "text", text: `\u274C \uD30C\uC77C \uC77D\uAE30 \uC2E4\uD328: ${e.message}` }], isError: true };
93
+ }
86
94
  }
87
95
  );
88
96
  server.tool(
@@ -105,9 +113,17 @@ ${error.stderr ?? ""}`
105
113
  path: import_zod.z.string().describe("\uB514\uB809\uD1A0\uB9AC \uACBD\uB85C")
106
114
  },
107
115
  async ({ path: dirPath }) => {
108
- const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true });
109
- const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
110
- return { content: [{ type: "text", text: lines.join("\n") }] };
116
+ try {
117
+ const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true });
118
+ const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
119
+ return { content: [{ type: "text", text: lines.join("\n") }] };
120
+ } catch (err) {
121
+ const e = err;
122
+ if (e.code === "ENOENT") {
123
+ return { content: [{ type: "text", text: `\u274C \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${dirPath}` }], isError: true };
124
+ }
125
+ return { content: [{ type: "text", text: `\u274C \uB514\uB809\uD1A0\uB9AC \uC77D\uAE30 \uC2E4\uD328: ${e.message}` }], isError: true };
126
+ }
111
127
  }
112
128
  );
113
129
  server.tool(
@@ -127,12 +143,13 @@ ${error.stderr ?? ""}`
127
143
  );
128
144
  return { content: [{ type: "text", text: stdout || "\uACB0\uACFC \uC5C6\uC74C" }] };
129
145
  } catch {
130
- const files = await (0, import_glob.glob)(file_pattern, { cwd: directory });
146
+ const safeDirectory = import_path.default.resolve(directory);
147
+ const files = await (0, import_glob.glob)(file_pattern, { cwd: safeDirectory });
131
148
  const results = [];
132
149
  for (const file of files.slice(0, 100)) {
133
150
  try {
134
151
  const content = await import_promises.default.readFile(
135
- import_path.default.join(directory, file),
152
+ import_path.default.join(safeDirectory, file),
136
153
  "utf-8"
137
154
  );
138
155
  const lines = content.split("\n");
@@ -370,12 +387,19 @@ var BrowserTools = class {
370
387
  "JavaScript \uC2E4\uD589",
371
388
  { code: import_zod2.z.string().describe("\uC2E4\uD589\uD560 JavaScript \uCF54\uB4DC") },
372
389
  ({ code }) => this.withLock(async () => {
373
- const result = await requirePage().evaluate(code);
374
- return {
375
- content: [
376
- { type: "text", text: JSON.stringify(result, null, 2) }
377
- ]
378
- };
390
+ try {
391
+ const result = await requirePage().evaluate(code);
392
+ return {
393
+ content: [
394
+ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }
395
+ ]
396
+ };
397
+ } catch (err) {
398
+ return {
399
+ content: [{ type: "text", text: `\u274C JavaScript \uC2E4\uD589 \uC624\uB958: ${err.message}` }],
400
+ isError: true
401
+ };
402
+ }
379
403
  })
380
404
  );
381
405
  server.tool(
@@ -401,7 +425,7 @@ async function readNotebook(filePath) {
401
425
  try {
402
426
  return JSON.parse(raw);
403
427
  } catch {
404
- throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 .ipynb \uD30C\uC77C: ${filePath}`);
428
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 Jupyter \uB178\uD2B8\uBD81 \uD30C\uC77C\uC785\uB2C8\uB2E4: ${filePath}`);
405
429
  }
406
430
  }
407
431
  async function writeNotebook(filePath, nb) {
@@ -497,16 +521,21 @@ var NotebookTools = class {
497
521
  execution_count: cellType === "code" ? null : void 0
498
522
  };
499
523
  let actualIndex;
524
+ let warning = "";
500
525
  if (position === void 0 || position === null) {
501
526
  nb.cells.push(newCell);
502
527
  actualIndex = nb.cells.length - 1;
528
+ } else if (position > nb.cells.length) {
529
+ nb.cells.push(newCell);
530
+ actualIndex = nb.cells.length - 1;
531
+ warning = ` (\uACBD\uACE0: position ${position}\uC774 \uBC94\uC704\uB97C \uCD08\uACFC\uD558\uC5EC \uB05D(index: ${actualIndex})\uC5D0 \uCD94\uAC00\uB428)`;
503
532
  } else {
504
- const clamped = Math.max(0, Math.min(position, nb.cells.length));
533
+ const clamped = Math.max(0, position);
505
534
  nb.cells.splice(clamped, 0, newCell);
506
535
  actualIndex = clamped;
507
536
  }
508
537
  await writeNotebook(filePath, nb);
509
- return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})` }] };
538
+ return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})${warning}` }] };
510
539
  }
511
540
  );
512
541
  server.tool(
@@ -564,9 +593,11 @@ var DeviceTools = class {
564
593
  }
565
594
  const { readFileSync, unlinkSync } = await import("fs");
566
595
  const data = readFileSync(tmpPath).toString("base64");
567
- if (isTmp) try {
568
- unlinkSync(tmpPath);
569
- } catch {
596
+ if (isTmp) {
597
+ try {
598
+ unlinkSync(tmpPath);
599
+ } catch {
600
+ }
570
601
  }
571
602
  return {
572
603
  content: [{ type: "image", data, mimeType: "image/png" }]
@@ -591,7 +622,14 @@ var DeviceTools = class {
591
622
  try {
592
623
  await execAsync3(cmd);
593
624
  } catch (err) {
594
- throw new Error(`\uCE74\uBA54\uB77C \uCD2C\uC601 \uC2E4\uD328: ${err.message}`);
625
+ const e = err;
626
+ return {
627
+ content: [{ type: "text", text: `\u274C \uCE74\uBA54\uB77C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uAC70\uB098 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
628
+ \uC6D0\uC778: ${e.message}
629
+
630
+ \uCE74\uBA54\uB77C\uAC00 \uC5F0\uACB0\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.` }],
631
+ isError: true
632
+ };
595
633
  }
596
634
  const { readFileSync, unlinkSync } = await import("fs");
597
635
  const data = readFileSync(tmpPath).toString("base64");
@@ -613,11 +651,17 @@ var DeviceTools = class {
613
651
  },
614
652
  async ({ title, message }) => {
615
653
  const p = platform();
616
- const cmd = {
617
- mac: `osascript -e 'display notification "${message}" with title "${title}"'`,
618
- 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())"`,
619
- linux: `notify-send "${title}" "${message}"`
620
- }[p];
654
+ let cmd;
655
+ if (p === "win") {
656
+ const script = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${message.replace(/'/g, "''")}', '${title.replace(/'/g, "''")}')`;
657
+ const encoded = Buffer.from(script, "utf16le").toString("base64");
658
+ cmd = `powershell -NoProfile -EncodedCommand ${encoded}`;
659
+ } else {
660
+ cmd = {
661
+ mac: `osascript -e 'display notification "${message.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"'`,
662
+ linux: `notify-send "${title.replace(/"/g, '\\"')}" "${message.replace(/"/g, '\\"')}"`
663
+ }[p] ?? "";
664
+ }
621
665
  await execAsync3(cmd);
622
666
  return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
623
667
  }
@@ -674,6 +718,7 @@ var DeviceTools = class {
674
718
  }
675
719
  try {
676
720
  process.kill(screenRecordPid, "SIGINT");
721
+ await new Promise((r) => setTimeout(r, 1e3));
677
722
  } catch {
678
723
  }
679
724
  screenRecordPid = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "junis",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "One-line device control for AI agents",
5
5
  "bin": {
6
6
  "junis": "dist/cli/index.js"