junis 0.2.5 → 0.3.1

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.
@@ -6,6 +6,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
9
13
  var __copyProps = (to, from, except, desc) => {
10
14
  if (from && typeof from === "object" || typeof from === "function") {
11
15
  for (let key of __getOwnPropNames(from))
@@ -22,8 +26,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
26
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
27
  mod
24
28
  ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
25
30
 
26
31
  // src/server/stdio.ts
32
+ var stdio_exports = {};
33
+ __export(stdio_exports, {
34
+ startStdioServer: () => startStdioServer
35
+ });
36
+ module.exports = __toCommonJS(stdio_exports);
27
37
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
28
38
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
29
39
 
@@ -34,29 +44,81 @@ var import_promises = __toESM(require("fs/promises"));
34
44
  var import_path = __toESM(require("path"));
35
45
  var import_glob = require("glob");
36
46
  var import_zod = require("zod");
47
+
48
+ // src/server/permissions.ts
49
+ var toolPermissions = {
50
+ // 읽기 전용 — 자동 허용
51
+ browser_snapshot: "auto",
52
+ browser_screenshot: "auto",
53
+ desktop_see: "auto",
54
+ desktop_list_apps: "auto",
55
+ desktop_list_windows: "auto",
56
+ cron_list: "auto",
57
+ read_file: "auto",
58
+ list_directory: "auto",
59
+ list_processes: "auto",
60
+ search_code: "auto",
61
+ // 상호작용 — 확인 권장 (현재: auto와 동일하게 실행, 향후 UI 연동)
62
+ browser_click: "confirm",
63
+ browser_type: "confirm",
64
+ browser_navigate: "confirm",
65
+ browser_fill: "confirm",
66
+ browser_select: "confirm",
67
+ browser_press: "confirm",
68
+ browser_hover: "confirm",
69
+ browser_drag: "confirm",
70
+ browser_upload: "confirm",
71
+ browser_cookies: "confirm",
72
+ browser_storage: "confirm",
73
+ browser_dialog: "confirm",
74
+ desktop_click: "confirm",
75
+ desktop_type: "confirm",
76
+ desktop_hotkey: "confirm",
77
+ desktop_scroll: "confirm",
78
+ desktop_menu: "confirm",
79
+ desktop_screenshot: "confirm",
80
+ cron_create: "confirm",
81
+ cron_delete: "confirm",
82
+ edit_block: "confirm",
83
+ kill_process: "confirm",
84
+ // 시스템 변경 — 기본 차단 (PDF 7.3절)
85
+ execute_command: "deny",
86
+ write_file: "deny"
87
+ };
88
+ function checkPermission(toolName) {
89
+ const level = toolPermissions[toolName];
90
+ if (level === "deny") {
91
+ throw new Error(
92
+ `Tool '${toolName}' is blocked by permission policy (deny). To allow, update toolPermissions in src/server/permissions.ts.`
93
+ );
94
+ }
95
+ }
96
+
97
+ // src/tools/filesystem.ts
37
98
  var execAsync = (0, import_util.promisify)(import_child_process.exec);
38
99
  var execFileAsync = (0, import_util.promisify)(import_child_process.execFile);
39
100
  var FilesystemTools = class {
40
101
  register(server) {
41
102
  server.tool(
42
103
  "execute_command",
43
- "\uD130\uBBF8\uB110 \uBA85\uB839 \uC2E4\uD589",
104
+ "Execute terminal command",
44
105
  {
45
- command: import_zod.z.string().describe("\uC2E4\uD589\uD560 \uC258 \uBA85\uB839"),
46
- timeout_ms: import_zod.z.number().optional().default(3e4).describe("\uD0C0\uC784\uC544\uC6C3 (ms)"),
47
- background: import_zod.z.boolean().optional().default(false).describe("\uBC31\uADF8\uB77C\uC6B4\uB4DC \uC2E4\uD589")
106
+ command: import_zod.z.string().describe("Shell command to execute"),
107
+ timeout_ms: import_zod.z.number().optional().default(3e4).describe("Timeout (ms)"),
108
+ background: import_zod.z.boolean().optional().default(false).describe("Run in background")
48
109
  },
49
110
  async ({ command, timeout_ms, background }) => {
111
+ checkPermission("execute_command");
50
112
  if (background) {
51
113
  (0, import_child_process.exec)(command);
52
- return { content: [{ type: "text", text: "\uBC31\uADF8\uB77C\uC6B4\uB4DC \uC2E4\uD589 \uC2DC\uC791\uB428" }] };
114
+ return { content: [{ type: "text", text: "Background execution started" }] };
53
115
  }
54
116
  try {
55
117
  const { stdout, stderr } = await execAsync(command, {
56
118
  timeout: timeout_ms
57
119
  });
58
120
  return {
59
- content: [{ type: "text", text: stdout || stderr || "(\uCD9C\uB825 \uC5C6\uC74C)" }]
121
+ content: [{ type: "text", text: stdout || stderr || "(no output)" }]
60
122
  };
61
123
  } catch (err) {
62
124
  const error = err;
@@ -64,7 +126,7 @@ var FilesystemTools = class {
64
126
  content: [
65
127
  {
66
128
  type: "text",
67
- text: `\uC624\uB958 (exit ${error.code ?? "?"}): ${error.message}
129
+ text: `Error (exit ${error.code ?? "?"}): ${error.message}
68
130
  ${error.stderr ?? ""}`
69
131
  }
70
132
  ],
@@ -75,10 +137,10 @@ ${error.stderr ?? ""}`
75
137
  );
76
138
  server.tool(
77
139
  "read_file",
78
- "\uD30C\uC77C \uC77D\uAE30",
140
+ "Read file",
79
141
  {
80
- path: import_zod.z.string().describe("\uD30C\uC77C \uACBD\uB85C"),
81
- encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("\uC778\uCF54\uB529")
142
+ path: import_zod.z.string().describe("File path"),
143
+ encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("Encoding")
82
144
  },
83
145
  async ({ path: filePath, encoding }) => {
84
146
  try {
@@ -87,30 +149,31 @@ ${error.stderr ?? ""}`
87
149
  } catch (err) {
88
150
  const e = err;
89
151
  if (e.code === "ENOENT") {
90
- return { content: [{ type: "text", text: `\u274C \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath}` }], isError: true };
152
+ return { content: [{ type: "text", text: `\u274C File not found: ${filePath}` }], isError: true };
91
153
  }
92
- return { content: [{ type: "text", text: `\u274C \uD30C\uC77C \uC77D\uAE30 \uC2E4\uD328: ${e.message}` }], isError: true };
154
+ return { content: [{ type: "text", text: `\u274C Failed to read file: ${e.message}` }], isError: true };
93
155
  }
94
156
  }
95
157
  );
96
158
  server.tool(
97
159
  "write_file",
98
- "\uD30C\uC77C \uC4F0\uAE30/\uC0DD\uC131",
160
+ "Write/create file",
99
161
  {
100
- path: import_zod.z.string().describe("\uD30C\uC77C \uACBD\uB85C"),
101
- content: import_zod.z.string().describe("\uD30C\uC77C \uB0B4\uC6A9")
162
+ path: import_zod.z.string().describe("File path"),
163
+ content: import_zod.z.string().describe("File content")
102
164
  },
103
165
  async ({ path: filePath, content }) => {
166
+ checkPermission("write_file");
104
167
  await import_promises.default.mkdir(import_path.default.dirname(filePath), { recursive: true });
105
168
  await import_promises.default.writeFile(filePath, content, "utf-8");
106
- return { content: [{ type: "text", text: "\uD30C\uC77C \uC800\uC7A5 \uC644\uB8CC" }] };
169
+ return { content: [{ type: "text", text: "File saved" }] };
107
170
  }
108
171
  );
109
172
  server.tool(
110
173
  "list_directory",
111
- "\uB514\uB809\uD1A0\uB9AC \uBAA9\uB85D \uC870\uD68C",
174
+ "List directory contents",
112
175
  {
113
- path: import_zod.z.string().describe("\uB514\uB809\uD1A0\uB9AC \uACBD\uB85C")
176
+ path: import_zod.z.string().describe("Directory path")
114
177
  },
115
178
  async ({ path: dirPath }) => {
116
179
  try {
@@ -120,19 +183,19 @@ ${error.stderr ?? ""}`
120
183
  } catch (err) {
121
184
  const e = err;
122
185
  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 };
186
+ return { content: [{ type: "text", text: `\u274C Directory not found: ${dirPath}` }], isError: true };
124
187
  }
125
- return { content: [{ type: "text", text: `\u274C \uB514\uB809\uD1A0\uB9AC \uC77D\uAE30 \uC2E4\uD328: ${e.message}` }], isError: true };
188
+ return { content: [{ type: "text", text: `\u274C Failed to read directory: ${e.message}` }], isError: true };
126
189
  }
127
190
  }
128
191
  );
129
192
  server.tool(
130
193
  "search_code",
131
- "\uCF54\uB4DC/\uD14D\uC2A4\uD2B8 \uAC80\uC0C9",
194
+ "Search code/text",
132
195
  {
133
- pattern: import_zod.z.string().describe("\uAC80\uC0C9 \uD328\uD134 (\uC815\uADDC\uC2DD \uC9C0\uC6D0)"),
134
- directory: import_zod.z.string().optional().default(".").describe("\uAC80\uC0C9 \uB514\uB809\uD1A0\uB9AC"),
135
- file_pattern: import_zod.z.string().optional().default("**/*").describe("\uD30C\uC77C \uD328\uD134")
196
+ pattern: import_zod.z.string().describe("Search pattern (regex supported)"),
197
+ directory: import_zod.z.string().optional().default(".").describe("Search directory"),
198
+ file_pattern: import_zod.z.string().optional().default("**/*").describe("File pattern")
136
199
  },
137
200
  async ({ pattern, directory, file_pattern }) => {
138
201
  try {
@@ -141,7 +204,7 @@ ${error.stderr ?? ""}`
141
204
  ["--no-heading", "-n", pattern, directory],
142
205
  { timeout: 1e4 }
143
206
  );
144
- return { content: [{ type: "text", text: stdout || "\uACB0\uACFC \uC5C6\uC74C" }] };
207
+ return { content: [{ type: "text", text: stdout || "No results" }] };
145
208
  } catch {
146
209
  const safeDirectory = import_path.default.resolve(directory);
147
210
  const files = await (0, import_glob.glob)(file_pattern, { cwd: safeDirectory });
@@ -162,7 +225,7 @@ ${error.stderr ?? ""}`
162
225
  }
163
226
  return {
164
227
  content: [
165
- { type: "text", text: results.join("\n") || "\uACB0\uACFC \uC5C6\uC74C" }
228
+ { type: "text", text: results.join("\n") || "No results" }
166
229
  ]
167
230
  };
168
231
  }
@@ -170,7 +233,7 @@ ${error.stderr ?? ""}`
170
233
  );
171
234
  server.tool(
172
235
  "list_processes",
173
- "\uC2E4\uD589 \uC911\uC778 \uD504\uB85C\uC138\uC2A4 \uBAA9\uB85D",
236
+ "List running processes",
174
237
  {},
175
238
  async () => {
176
239
  const cmd = process.platform === "win32" ? "tasklist" : process.platform === "darwin" ? "ps aux | sort -rk 3 | head -30" : "ps aux --sort=-%cpu | head -30";
@@ -180,23 +243,23 @@ ${error.stderr ?? ""}`
180
243
  );
181
244
  server.tool(
182
245
  "kill_process",
183
- "\uD504\uB85C\uC138\uC2A4 \uC885\uB8CC (SIGTERM \uD6C4 3\uCD08 \uB300\uAE30, \uC0B4\uC544\uC788\uC73C\uBA74 SIGKILL \uC790\uB3D9 \uC801\uC6A9)",
246
+ "Kill process (SIGTERM then 3s wait, auto SIGKILL if still alive)",
184
247
  {
185
- pid: import_zod.z.number().describe("\uC885\uB8CC\uD560 \uD504\uB85C\uC138\uC2A4 PID"),
186
- signal: import_zod.z.enum(["SIGTERM", "SIGKILL"]).optional().default("SIGTERM").describe("\uCD08\uAE30 \uC2DC\uADF8\uB110 (\uAE30\uBCF8\uAC12: SIGTERM). SIGKILL \uC9C0\uC815 \uC2DC \uC989\uC2DC \uAC15\uC81C \uC885\uB8CC)")
248
+ pid: import_zod.z.number().describe("PID of the process to kill"),
249
+ signal: import_zod.z.enum(["SIGTERM", "SIGKILL"]).optional().default("SIGTERM").describe("Initial signal (default: SIGTERM). SIGKILL for immediate force kill)")
187
250
  },
188
251
  async ({ pid, signal }) => {
189
252
  const isWindows = process.platform === "win32";
190
253
  if (isWindows) {
191
254
  await execAsync(`taskkill /PID ${pid} /F`);
192
255
  return {
193
- content: [{ type: "text", text: `PID ${pid} \uC885\uB8CC \uC644\uB8CC (taskkill /F)` }]
256
+ content: [{ type: "text", text: `PID ${pid} killed (taskkill /F)` }]
194
257
  };
195
258
  }
196
259
  if (signal === "SIGKILL") {
197
260
  await execAsync(`kill -9 ${pid}`);
198
261
  return {
199
- content: [{ type: "text", text: `PID ${pid} \uAC15\uC81C \uC885\uB8CC \uC644\uB8CC (SIGKILL)` }]
262
+ content: [{ type: "text", text: `PID ${pid} force killed (SIGKILL)` }]
200
263
  };
201
264
  }
202
265
  try {
@@ -204,7 +267,7 @@ ${error.stderr ?? ""}`
204
267
  } catch {
205
268
  return {
206
269
  content: [
207
- { type: "text", text: `PID ${pid} \uC885\uB8CC \uC2E4\uD328: \uD504\uB85C\uC138\uC2A4\uAC00 \uC874\uC7AC\uD558\uC9C0 \uC54A\uAC70\uB098 \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.` }
270
+ { type: "text", text: `PID ${pid} kill failed: process does not exist or permission denied.` }
208
271
  ],
209
272
  isError: true
210
273
  };
@@ -213,7 +276,7 @@ ${error.stderr ?? ""}`
213
276
  const isAlive = await execAsync(`kill -0 ${pid}`).then(() => true).catch(() => false);
214
277
  if (!isAlive) {
215
278
  return {
216
- content: [{ type: "text", text: `PID ${pid} \uC885\uB8CC \uC644\uB8CC (SIGTERM)` }]
279
+ content: [{ type: "text", text: `PID ${pid} killed (SIGTERM)` }]
217
280
  };
218
281
  }
219
282
  await execAsync(`kill -9 ${pid}`);
@@ -221,7 +284,7 @@ ${error.stderr ?? ""}`
221
284
  content: [
222
285
  {
223
286
  type: "text",
224
- text: `PID ${pid} \uAC15\uC81C \uC885\uB8CC \uC644\uB8CC (SIGTERM \uBB34\uC751\uB2F5 \u2192 SIGKILL \uC790\uB3D9 \uC801\uC6A9)`
287
+ text: `PID ${pid} force killed (SIGTERM unresponsive, auto SIGKILL applied)`
225
288
  }
226
289
  ]
227
290
  };
@@ -229,17 +292,17 @@ ${error.stderr ?? ""}`
229
292
  );
230
293
  server.tool(
231
294
  "edit_block",
232
- "\uD30C\uC77C\uC758 \uD2B9\uC815 \uD14D\uC2A4\uD2B8 \uBE14\uB85D\uC744 \uC0C8 \uD14D\uC2A4\uD2B8\uB85C \uAD50\uCCB4 (diff \uAE30\uBC18 \uBD80\uBD84 \uC218\uC815)",
295
+ "Replace a specific text block in a file with new text (diff-based partial edit)",
233
296
  {
234
- path: import_zod.z.string().describe("\uD30C\uC77C \uACBD\uB85C"),
235
- old_string: import_zod.z.string().describe("\uAD50\uCCB4\uD560 \uAE30\uC874 \uD14D\uC2A4\uD2B8 (\uC815\uD655\uD788 \uC77C\uCE58\uD574\uC57C \uD568)"),
236
- new_string: import_zod.z.string().describe("\uC0C8 \uD14D\uC2A4\uD2B8"),
237
- replace_all: import_zod.z.boolean().optional().default(false).describe("true\uBA74 \uBAA8\uB4E0 \uB9E4\uCE6D \uAD50\uCCB4, false\uBA74 \uCCAB \uBC88\uC9F8\uB9CC")
297
+ path: import_zod.z.string().describe("File path"),
298
+ old_string: import_zod.z.string().describe("Existing text to replace (must match exactly)"),
299
+ new_string: import_zod.z.string().describe("New text"),
300
+ replace_all: import_zod.z.boolean().optional().default(false).describe("If true, replace all matches; if false, replace only the first")
238
301
  },
239
302
  async ({ path: filePath, old_string, new_string, replace_all }) => {
240
303
  const content = await import_promises.default.readFile(filePath, "utf-8");
241
304
  if (!content.includes(old_string)) {
242
- throw new Error(`old_string\uC744 \uD30C\uC77C\uC5D0\uC11C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath}`);
305
+ throw new Error(`old_string not found in file: ${filePath}`);
243
306
  }
244
307
  let count = 0;
245
308
  let pos = 0;
@@ -249,7 +312,7 @@ ${error.stderr ?? ""}`
249
312
  }
250
313
  if (!replace_all && count > 1) {
251
314
  throw new Error(
252
- `\uB9E4\uCE6D\uC774 ${count}\uAC1C\uC785\uB2C8\uB2E4. replace_all\uC744 true\uB85C \uD558\uAC70\uB098 \uB354 \uB113\uC740 \uCEE8\uD14D\uC2A4\uD2B8\uB97C \uD3EC\uD568\uD558\uC138\uC694.`
315
+ `Found ${count} matches. Set replace_all to true or include more context to narrow it down.`
253
316
  );
254
317
  }
255
318
  let result;
@@ -263,21 +326,197 @@ ${error.stderr ?? ""}`
263
326
  }
264
327
  await import_promises.default.writeFile(filePath, result, "utf-8");
265
328
  return {
266
- content: [{ type: "text", text: `\uAD50\uCCB4 \uC644\uB8CC (${replaced}\uAC1C \uBCC0\uACBD\uB428)` }]
329
+ content: [{ type: "text", text: `Replaced (${replaced} occurrence(s) changed)` }]
267
330
  };
268
331
  }
269
332
  );
333
+ server.tool(
334
+ "cron_create",
335
+ "Create a recurring cron job. schedule uses cron syntax (e.g. '0 9 * * 1-5' = weekdays 9am).",
336
+ {
337
+ schedule: import_zod.z.string().describe("Cron schedule expression (e.g. '*/5 * * * *' for every 5 min, '0 9 * * 1-5' for weekdays 9am)"),
338
+ command: import_zod.z.string().describe("Shell command to execute"),
339
+ label: import_zod.z.string().optional().describe("Optional label/comment for identification")
340
+ },
341
+ async ({ schedule, command, label }) => {
342
+ try {
343
+ let existing = "";
344
+ try {
345
+ const { stdout } = await execAsync("crontab -l");
346
+ existing = stdout;
347
+ } catch {
348
+ }
349
+ if (existing.includes(command)) {
350
+ return {
351
+ content: [{ type: "text", text: `\u26A0\uFE0F A cron job with this command already exists.` }],
352
+ isError: true
353
+ };
354
+ }
355
+ const comment = label ? `# junis:${label}
356
+ ` : "# junis-cron\n";
357
+ const newEntry = `${comment}${schedule} ${command}
358
+ `;
359
+ const updated = existing.trimEnd() + "\n" + newEntry;
360
+ const tmpFile = `/tmp/junis_crontab_${Date.now()}.txt`;
361
+ await import_promises.default.writeFile(tmpFile, updated, "utf-8");
362
+ await execAsync(`crontab ${tmpFile}`);
363
+ await import_promises.default.unlink(tmpFile).catch(() => {
364
+ });
365
+ return {
366
+ content: [{ type: "text", text: `\u2705 Cron job created:
367
+ schedule: ${schedule}
368
+ command: ${command}${label ? `
369
+ label: ${label}` : ""}` }]
370
+ };
371
+ } catch (err) {
372
+ return {
373
+ content: [{ type: "text", text: `\u274C Failed to create cron job: ${err.message}` }],
374
+ isError: true
375
+ };
376
+ }
377
+ }
378
+ );
379
+ server.tool(
380
+ "cron_list",
381
+ "List all cron jobs in the current user's crontab",
382
+ {},
383
+ async () => {
384
+ try {
385
+ const { stdout } = await execAsync("crontab -l");
386
+ const lines = stdout.trim().split("\n").filter((l) => l.trim());
387
+ if (lines.length === 0) {
388
+ return { content: [{ type: "text", text: "No cron jobs found." }] };
389
+ }
390
+ const entries = [];
391
+ let pendingLabel;
392
+ let id = 1;
393
+ for (const line of lines) {
394
+ if (line.startsWith("#")) {
395
+ const match = line.match(/^# junis:(.+)$/);
396
+ pendingLabel = match ? match[1].trim() : void 0;
397
+ continue;
398
+ }
399
+ const parts = line.split(/\s+/);
400
+ if (parts.length >= 6) {
401
+ const schedule = parts.slice(0, 5).join(" ");
402
+ const command = parts.slice(5).join(" ");
403
+ entries.push({ id: id++, label: pendingLabel, schedule, command });
404
+ }
405
+ pendingLabel = void 0;
406
+ }
407
+ if (entries.length === 0) {
408
+ return { content: [{ type: "text", text: stdout }] };
409
+ }
410
+ const output = entries.map(
411
+ (e) => `[${e.id}] ${e.label ? `(${e.label}) ` : ""}${e.schedule} \u2192 ${e.command}`
412
+ ).join("\n");
413
+ return { content: [{ type: "text", text: output }] };
414
+ } catch (err) {
415
+ const e = err;
416
+ if (e.code === 1) {
417
+ return { content: [{ type: "text", text: "No cron jobs found (crontab is empty)." }] };
418
+ }
419
+ return {
420
+ content: [{ type: "text", text: `\u274C Failed to list cron jobs: ${e.message}` }],
421
+ isError: true
422
+ };
423
+ }
424
+ }
425
+ );
426
+ server.tool(
427
+ "cron_delete",
428
+ "Delete a cron job by its ID (from cron_list) or by matching command string",
429
+ {
430
+ id: import_zod.z.number().optional().describe("Cron job ID from cron_list output"),
431
+ command: import_zod.z.string().optional().describe("Delete job matching this command string")
432
+ },
433
+ async ({ id, command }) => {
434
+ if (!id && !command) {
435
+ return {
436
+ content: [{ type: "text", text: "\u274C Provide either id or command to identify the cron job." }],
437
+ isError: true
438
+ };
439
+ }
440
+ try {
441
+ let existing = "";
442
+ try {
443
+ const { stdout } = await execAsync("crontab -l");
444
+ existing = stdout;
445
+ } catch {
446
+ return { content: [{ type: "text", text: "No cron jobs to delete." }] };
447
+ }
448
+ const lines = existing.split("\n");
449
+ if (command) {
450
+ const filtered2 = [];
451
+ for (let i = 0; i < lines.length; i++) {
452
+ if (lines[i].includes(command)) {
453
+ if (filtered2.length > 0 && filtered2[filtered2.length - 1].trim().startsWith("#")) {
454
+ filtered2.pop();
455
+ }
456
+ continue;
457
+ }
458
+ filtered2.push(lines[i]);
459
+ }
460
+ if (filtered2.length === lines.length) {
461
+ return {
462
+ content: [{ type: "text", text: `\u274C No cron job found matching: ${command}` }],
463
+ isError: true
464
+ };
465
+ }
466
+ const updated2 = filtered2.join("\n");
467
+ const tmpFile2 = `/tmp/junis_crontab_${Date.now()}.txt`;
468
+ await import_promises.default.writeFile(tmpFile2, updated2, "utf-8");
469
+ await execAsync(`crontab ${tmpFile2}`);
470
+ await import_promises.default.unlink(tmpFile2).catch(() => {
471
+ });
472
+ return { content: [{ type: "text", text: `\u2705 Deleted cron job matching: ${command}` }] };
473
+ }
474
+ const entries = [];
475
+ let idx = 1;
476
+ for (let i = 0; i < lines.length; i++) {
477
+ const line = lines[i].trim();
478
+ if (line.startsWith("#")) continue;
479
+ const parts = line.split(/\s+/);
480
+ if (parts.length >= 6) {
481
+ const prevIsComment = i > 0 && lines[i - 1].trim().startsWith("#");
482
+ entries.push({ lineStart: prevIsComment ? i - 1 : i, lineEnd: i, idx: idx++ });
483
+ }
484
+ }
485
+ const target = entries.find((e) => e.idx === id);
486
+ if (!target) {
487
+ return {
488
+ content: [{ type: "text", text: `\u274C No cron job found with id=${id}. Use cron_list to see current IDs.` }],
489
+ isError: true
490
+ };
491
+ }
492
+ const filtered = lines.filter((_, i) => i < target.lineStart || i > target.lineEnd);
493
+ const updated = filtered.join("\n");
494
+ const tmpFile = `/tmp/junis_crontab_${Date.now()}.txt`;
495
+ await import_promises.default.writeFile(tmpFile, updated, "utf-8");
496
+ await execAsync(`crontab ${tmpFile}`);
497
+ await import_promises.default.unlink(tmpFile).catch(() => {
498
+ });
499
+ return { content: [{ type: "text", text: `\u2705 Deleted cron job #${id}` }] };
500
+ } catch (err) {
501
+ return {
502
+ content: [{ type: "text", text: `\u274C Failed to delete cron job: ${err.message}` }],
503
+ isError: true
504
+ };
505
+ }
506
+ }
507
+ );
270
508
  }
271
509
  };
272
510
 
273
511
  // src/tools/browser.ts
274
- var import_playwright = require("playwright");
512
+ var import_browserclaw = require("browserclaw");
513
+ var import_promises2 = __toESM(require("fs/promises"));
275
514
  var import_zod2 = require("zod");
276
515
  var BrowserTools = class {
277
516
  browser = null;
278
517
  page = null;
279
- // 동시 요청 시 race condition 방지용 직렬화 락
280
518
  lock = Promise.resolve();
519
+ armedDialog = null;
281
520
  withLock(fn) {
282
521
  let release;
283
522
  const next = new Promise((r) => {
@@ -287,128 +526,373 @@ var BrowserTools = class {
287
526
  this.lock = this.lock.then(() => next);
288
527
  return current.then(() => fn()).finally(() => release());
289
528
  }
529
+ /** mcp.ts에서 호출하는 init — BrowserClaw는 browser_start 도구로 명시적 시작하므로 noop */
290
530
  async init() {
291
- try {
292
- this.browser = await import_playwright.chromium.launch({ headless: true });
293
- this.page = await this.browser.newPage();
294
- } catch {
295
- console.warn(
296
- "\u26A0\uFE0F Playwright \uBBF8\uC124\uCE58. \uBE0C\uB77C\uC6B0\uC800 \uB3C4\uAD6C \uBE44\uD65C\uC131\uD654.\n \uD65C\uC131\uD654: npx playwright install chromium"
297
- );
298
- }
299
531
  }
300
532
  async cleanup() {
301
- await this.browser?.close();
533
+ await this.browser?.stop();
534
+ this.browser = null;
535
+ this.page = null;
302
536
  }
303
537
  register(server) {
304
538
  const requirePage = () => {
305
- if (!this.page) throw new Error("\uBE0C\uB77C\uC6B0\uC800 \uBBF8\uCD08\uAE30\uD654. playwright \uC124\uCE58 \uD655\uC778.");
539
+ if (!this.page) throw new Error("Browser not started. Call browser_start first.");
306
540
  return this.page;
307
541
  };
542
+ server.tool(
543
+ "browser_start",
544
+ "Start browser (BrowserClaw). mode='managed'(default) launches new Chromium; mode='remote-cdp' connects to existing Chrome via CDP URL.",
545
+ {
546
+ mode: import_zod2.z.enum(["managed", "remote-cdp"]).optional().default("managed").describe("'managed' = launch new browser, 'remote-cdp' = connect to existing Chrome"),
547
+ headless: import_zod2.z.boolean().optional().default(false).describe("Run headless (managed mode only)"),
548
+ cdpUrl: import_zod2.z.string().optional().describe("CDP URL for remote-cdp mode (e.g. http://localhost:9222)"),
549
+ profile: import_zod2.z.string().optional().describe("Profile name (managed mode only)"),
550
+ allowInternal: import_zod2.z.boolean().optional().default(false).describe("Allow localhost/internal URLs")
551
+ },
552
+ ({ mode, headless, cdpUrl, profile, allowInternal }) => this.withLock(async () => {
553
+ if (this.browser) {
554
+ return { content: [{ type: "text", text: "Browser is already running. Call browser_stop first." }] };
555
+ }
556
+ if (mode === "remote-cdp") {
557
+ if (!cdpUrl) throw new Error("cdpUrl is required for remote-cdp mode");
558
+ this.browser = await import_browserclaw.BrowserClaw.connect(cdpUrl, { allowInternal });
559
+ } else {
560
+ this.browser = await import_browserclaw.BrowserClaw.launch({
561
+ headless,
562
+ profileName: profile,
563
+ allowInternal
564
+ });
565
+ }
566
+ return { content: [{ type: "text", text: `Browser started (mode: ${mode})` }] };
567
+ })
568
+ );
569
+ server.tool(
570
+ "browser_stop",
571
+ "Stop browser and release resources",
572
+ {},
573
+ () => this.withLock(async () => {
574
+ await this.cleanup();
575
+ return { content: [{ type: "text", text: "Browser stopped" }] };
576
+ })
577
+ );
308
578
  server.tool(
309
579
  "browser_navigate",
310
- "URL\uB85C \uC774\uB3D9",
311
- { url: import_zod2.z.string().describe("\uC774\uB3D9\uD560 URL") },
580
+ "Navigate to URL. Opens new tab if browser started but no page yet.",
581
+ {
582
+ url: import_zod2.z.string().describe("URL to navigate to")
583
+ },
312
584
  ({ url }) => this.withLock(async () => {
313
- const page = requirePage();
314
- await page.goto(url, { waitUntil: "domcontentloaded" });
585
+ if (!this.browser) throw new Error("Browser not started. Call browser_start first.");
586
+ if (!this.page) {
587
+ this.page = await this.browser.open(url);
588
+ } else {
589
+ await this.page.goto(url);
590
+ }
591
+ const currentUrl = await this.page.url();
592
+ return { content: [{ type: "text", text: `Navigated to: ${currentUrl}` }] };
593
+ })
594
+ );
595
+ server.tool(
596
+ "browser_snapshot",
597
+ "Get Accessibility Tree snapshot with ref numbers. Use refs to interact with elements (e.g. browser_click with ref='e1').",
598
+ {
599
+ interactive: import_zod2.z.boolean().optional().default(true).describe("Only include interactive elements"),
600
+ compact: import_zod2.z.boolean().optional().default(true).describe("Remove empty containers")
601
+ },
602
+ ({ interactive, compact }) => this.withLock(async () => {
603
+ const result = await requirePage().snapshot({ interactive, compact });
604
+ const { snapshot, refs, stats } = result;
605
+ const refList = Object.entries(refs).map(([r, info]) => ` ${r}: ${info.role} "${info.name ?? ""}"`).join("\n");
606
+ const total = stats?.refs ?? Object.keys(refs).length;
315
607
  return {
316
- content: [{ type: "text", text: `\uC774\uB3D9 \uC644\uB8CC: ${page.url()}` }]
608
+ content: [{
609
+ type: "text",
610
+ text: `${snapshot}
611
+
612
+ --- refs (${total} total) ---
613
+ ${refList}`
614
+ }]
317
615
  };
318
616
  })
319
617
  );
320
618
  server.tool(
321
619
  "browser_click",
322
- "\uC694\uC18C \uD074\uB9AD",
323
- { selector: import_zod2.z.string().describe("CSS \uC120\uD0DD\uC790") },
324
- ({ selector }) => this.withLock(async () => {
325
- await requirePage().click(selector);
326
- return { content: [{ type: "text", text: "\uD074\uB9AD \uC644\uB8CC" }] };
620
+ "Click element by ref number from browser_snapshot",
621
+ {
622
+ ref: import_zod2.z.string().describe("Ref number from snapshot (e.g. 'e1')"),
623
+ doubleClick: import_zod2.z.boolean().optional().default(false),
624
+ button: import_zod2.z.enum(["left", "right", "middle"]).optional().default("left")
625
+ },
626
+ ({ ref, doubleClick, button }) => this.withLock(async () => {
627
+ await requirePage().click(ref, { doubleClick, button });
628
+ return { content: [{ type: "text", text: `Clicked ref=${ref}` }] };
327
629
  })
328
630
  );
329
631
  server.tool(
330
632
  "browser_type",
331
- "\uD14D\uC2A4\uD2B8 \uC785\uB825",
633
+ "Type text into element by ref number",
332
634
  {
333
- selector: import_zod2.z.string().describe("CSS \uC120\uD0DD\uC790"),
334
- text: import_zod2.z.string().describe("\uC785\uB825\uD560 \uD14D\uC2A4\uD2B8"),
335
- clear: import_zod2.z.boolean().optional().default(false).describe("\uAE30\uC874 \uB0B4\uC6A9 \uC0AD\uC81C \uD6C4 \uC785\uB825")
635
+ ref: import_zod2.z.string().describe("Ref number from snapshot"),
636
+ text: import_zod2.z.string().describe("Text to type"),
637
+ submit: import_zod2.z.boolean().optional().default(false).describe("Press Enter after typing"),
638
+ slowly: import_zod2.z.boolean().optional().default(false).describe("Type slowly (75ms per char)")
336
639
  },
337
- ({ selector, text, clear }) => this.withLock(async () => {
338
- const page = requirePage();
339
- if (clear) await page.fill(selector, text);
340
- else await page.type(selector, text);
341
- return { content: [{ type: "text", text: "\uC785\uB825 \uC644\uB8CC" }] };
640
+ ({ ref, text, submit, slowly }) => this.withLock(async () => {
641
+ await requirePage().type(ref, text, { submit, slowly });
642
+ return { content: [{ type: "text", text: `Typed into ref=${ref}` }] };
643
+ })
644
+ );
645
+ server.tool(
646
+ "browser_fill",
647
+ "Fill multiple form fields at once",
648
+ {
649
+ fields: import_zod2.z.array(import_zod2.z.object({
650
+ ref: import_zod2.z.string(),
651
+ type: import_zod2.z.enum(["text", "checkbox", "radio"]),
652
+ value: import_zod2.z.union([import_zod2.z.string(), import_zod2.z.boolean()])
653
+ })).describe("Array of {ref, type, value}")
654
+ },
655
+ ({ fields }) => this.withLock(async () => {
656
+ await requirePage().fill(fields);
657
+ return { content: [{ type: "text", text: `Filled ${fields.length} field(s)` }] };
658
+ })
659
+ );
660
+ server.tool(
661
+ "browser_select",
662
+ "Select dropdown option(s) by ref",
663
+ {
664
+ ref: import_zod2.z.string().describe("Ref number from snapshot"),
665
+ values: import_zod2.z.array(import_zod2.z.string()).describe("Option value(s) to select")
666
+ },
667
+ ({ ref, values }) => this.withLock(async () => {
668
+ await requirePage().select(ref, ...values);
669
+ return { content: [{ type: "text", text: `Selected option(s) in ref=${ref}` }] };
670
+ })
671
+ );
672
+ server.tool(
673
+ "browser_press",
674
+ "Press keyboard key or combination (e.g. 'Enter', 'Control+a', 'Escape')",
675
+ {
676
+ key: import_zod2.z.string().describe("Key combination (e.g. 'Enter', 'Control+a', 'Escape', 'Tab')")
677
+ },
678
+ ({ key }) => this.withLock(async () => {
679
+ await requirePage().press(key);
680
+ return { content: [{ type: "text", text: `Pressed: ${key}` }] };
681
+ })
682
+ );
683
+ server.tool(
684
+ "browser_hover",
685
+ "Hover mouse over element by ref",
686
+ {
687
+ ref: import_zod2.z.string().describe("Ref number from snapshot")
688
+ },
689
+ ({ ref }) => this.withLock(async () => {
690
+ await requirePage().hover(ref);
691
+ return { content: [{ type: "text", text: `Hovered over ref=${ref}` }] };
692
+ })
693
+ );
694
+ server.tool(
695
+ "browser_drag",
696
+ "Drag element from startRef to endRef",
697
+ {
698
+ startRef: import_zod2.z.string().describe("Source element ref"),
699
+ endRef: import_zod2.z.string().describe("Target element ref")
700
+ },
701
+ ({ startRef, endRef }) => this.withLock(async () => {
702
+ await requirePage().drag(startRef, endRef);
703
+ return { content: [{ type: "text", text: `Dragged ref=${startRef} \u2192 ref=${endRef}` }] };
704
+ })
705
+ );
706
+ server.tool(
707
+ "browser_upload",
708
+ "Upload file(s) to file input element by ref",
709
+ {
710
+ ref: import_zod2.z.string().describe("Ref number of file input element"),
711
+ paths: import_zod2.z.array(import_zod2.z.string()).describe("Absolute file path(s) to upload")
712
+ },
713
+ ({ ref, paths }) => this.withLock(async () => {
714
+ await requirePage().uploadFile(ref, paths);
715
+ return { content: [{ type: "text", text: `Uploaded ${paths.length} file(s) to ref=${ref}` }] };
342
716
  })
343
717
  );
344
718
  server.tool(
345
719
  "browser_screenshot",
346
- "\uD604\uC7AC \uD398\uC774\uC9C0 \uC2A4\uD06C\uB9B0\uC0F7",
720
+ "Take screenshot of current page",
347
721
  {
348
- path: import_zod2.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (\uC5C6\uC73C\uBA74 base64 \uBC18\uD658)"),
349
- full_page: import_zod2.z.boolean().optional().default(false)
722
+ path: import_zod2.z.string().optional().describe("Save path (if omitted, returns base64)"),
723
+ fullPage: import_zod2.z.boolean().optional().default(false),
724
+ ref: import_zod2.z.string().optional().describe("Capture specific element by ref")
350
725
  },
351
- ({ path: path2, full_page }) => this.withLock(async () => {
352
- const page = requirePage();
353
- const screenshot = await page.screenshot({
354
- path: path2 ?? void 0,
355
- fullPage: full_page
356
- });
726
+ ({ path: path2, fullPage, ref }) => this.withLock(async () => {
727
+ const buffer = await requirePage().screenshot({ fullPage, ref });
357
728
  if (path2) {
358
- return { content: [{ type: "text", text: `\uC800\uC7A5 \uC644\uB8CC: ${path2}` }] };
729
+ await import_promises2.default.writeFile(path2, buffer);
730
+ return { content: [{ type: "text", text: `Screenshot saved: ${path2}` }] };
359
731
  }
360
732
  return {
361
- content: [
362
- {
363
- type: "image",
364
- data: screenshot.toString("base64"),
365
- mimeType: "image/png"
366
- }
367
- ]
733
+ content: [{
734
+ type: "image",
735
+ data: buffer.toString("base64"),
736
+ mimeType: "image/png"
737
+ }]
368
738
  };
369
739
  })
370
740
  );
371
741
  server.tool(
372
- "browser_snapshot",
373
- "\uD398\uC774\uC9C0 \uC811\uADFC\uC131 \uD2B8\uB9AC \uC870\uD68C (\uAD6C\uC870 \uD30C\uC545\uC6A9)",
374
- {},
375
- () => this.withLock(async () => {
376
- const page = requirePage();
377
- const snapshot = await page.locator("body").ariaSnapshot();
378
- return {
379
- content: [
380
- { type: "text", text: snapshot }
381
- ]
382
- };
742
+ "browser_pdf",
743
+ "Save current page as PDF",
744
+ {
745
+ path: import_zod2.z.string().describe("Save path (.pdf)")
746
+ },
747
+ ({ path: path2 }) => this.withLock(async () => {
748
+ const buffer = await requirePage().pdf();
749
+ await import_promises2.default.writeFile(path2, buffer);
750
+ return { content: [{ type: "text", text: `PDF saved: ${path2}` }] };
383
751
  })
384
752
  );
385
753
  server.tool(
386
754
  "browser_evaluate",
387
- "JavaScript \uC2E4\uD589",
388
- { code: import_zod2.z.string().describe("\uC2E4\uD589\uD560 JavaScript \uCF54\uB4DC") },
755
+ "Execute JavaScript in page context",
756
+ {
757
+ code: import_zod2.z.string().describe("JavaScript code to execute (wrap in function if needed)")
758
+ },
389
759
  ({ code }) => this.withLock(async () => {
390
760
  try {
391
761
  const result = await requirePage().evaluate(code);
392
762
  return {
393
- content: [
394
- { type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }
395
- ]
763
+ content: [{
764
+ type: "text",
765
+ text: typeof result === "string" ? result : JSON.stringify(result, null, 2)
766
+ }]
396
767
  };
397
768
  } catch (err) {
398
769
  return {
399
- content: [{ type: "text", text: `\u274C JavaScript \uC2E4\uD589 \uC624\uB958: ${err.message}` }],
770
+ content: [{ type: "text", text: `\u274C JS error: ${err.message}` }],
400
771
  isError: true
401
772
  };
402
773
  }
403
774
  })
404
775
  );
405
776
  server.tool(
406
- "browser_pdf",
407
- "\uD604\uC7AC \uD398\uC774\uC9C0 PDF \uC800\uC7A5",
408
- { path: import_zod2.z.string().describe("\uC800\uC7A5 \uACBD\uB85C (.pdf)") },
409
- ({ path: path2 }) => this.withLock(async () => {
410
- await requirePage().pdf({ path: path2 });
411
- return { content: [{ type: "text", text: `PDF \uC800\uC7A5 \uC644\uB8CC: ${path2}` }] };
777
+ "browser_wait",
778
+ "Wait for a condition: text appearance/disappearance, URL pattern, or fixed time",
779
+ {
780
+ text: import_zod2.z.string().optional().describe("Wait until this text appears"),
781
+ textGone: import_zod2.z.string().optional().describe("Wait until this text disappears"),
782
+ url: import_zod2.z.string().optional().describe("Wait until URL matches (glob pattern, e.g. '**/dashboard')"),
783
+ loadState: import_zod2.z.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Wait for load state"),
784
+ timeMs: import_zod2.z.number().optional().describe("Wait fixed milliseconds")
785
+ },
786
+ ({ text, textGone, url, loadState, timeMs }) => this.withLock(async () => {
787
+ const condition = {};
788
+ if (text) condition.text = text;
789
+ if (textGone) condition.textGone = textGone;
790
+ if (url) condition.url = url;
791
+ if (loadState) condition.loadState = loadState;
792
+ if (timeMs) condition.timeMs = timeMs;
793
+ await requirePage().waitFor(condition);
794
+ return { content: [{ type: "text", text: "Wait condition met" }] };
795
+ })
796
+ );
797
+ server.tool(
798
+ "browser_cookies",
799
+ "Get, set, or clear cookies",
800
+ {
801
+ action: import_zod2.z.enum(["get", "set", "clear"]).describe("Action to perform"),
802
+ cookie: import_zod2.z.object({
803
+ name: import_zod2.z.string(),
804
+ value: import_zod2.z.string(),
805
+ domain: import_zod2.z.string().optional(),
806
+ path: import_zod2.z.string().optional(),
807
+ httpOnly: import_zod2.z.boolean().optional(),
808
+ secure: import_zod2.z.boolean().optional()
809
+ }).optional().describe("Cookie data (required for set action)")
810
+ },
811
+ ({ action, cookie }) => this.withLock(async () => {
812
+ const page = requirePage();
813
+ if (action === "get") {
814
+ const cookies = await page.cookies();
815
+ return { content: [{ type: "text", text: JSON.stringify(cookies, null, 2) }] };
816
+ } else if (action === "set") {
817
+ if (!cookie) throw new Error("cookie is required for set action");
818
+ await page.setCookie({ path: "/", ...cookie });
819
+ return { content: [{ type: "text", text: `Cookie set: ${cookie.name}` }] };
820
+ } else {
821
+ await page.clearCookies();
822
+ return { content: [{ type: "text", text: "All cookies cleared" }] };
823
+ }
824
+ })
825
+ );
826
+ server.tool(
827
+ "browser_storage",
828
+ "Read/write/clear localStorage or sessionStorage",
829
+ {
830
+ action: import_zod2.z.enum(["get", "set", "clear"]).describe("Action to perform"),
831
+ kind: import_zod2.z.enum(["local", "session"]).optional().default("local").describe("Storage type"),
832
+ key: import_zod2.z.string().optional().describe("Storage key (get/set)"),
833
+ value: import_zod2.z.string().optional().describe("Value to set (set action)")
834
+ },
835
+ ({ action, kind, key, value }) => this.withLock(async () => {
836
+ const page = requirePage();
837
+ if (action === "get") {
838
+ const result = await page.storageGet(kind, key);
839
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
840
+ } else if (action === "set") {
841
+ if (!key || value === void 0) throw new Error("key and value are required for set action");
842
+ await page.storageSet(kind, key, value);
843
+ return { content: [{ type: "text", text: `Storage set: ${key}` }] };
844
+ } else {
845
+ await page.storageClear(kind);
846
+ return { content: [{ type: "text", text: `${kind}Storage cleared` }] };
847
+ }
848
+ })
849
+ );
850
+ server.tool(
851
+ "browser_dialog",
852
+ [
853
+ "Handle JavaScript dialogs (alert/confirm/prompt).",
854
+ "Two-step usage:",
855
+ " 1. action='arm' \u2014 register a one-shot handler (returns immediately, does NOT block).",
856
+ " 2. Trigger the dialog (e.g. browser_click on the button that calls confirm()).",
857
+ " 3. action='wait' \u2014 await the handler to confirm the dialog was handled.",
858
+ "The 'accept' and 'promptText' params are only used with action='arm'."
859
+ ].join(" "),
860
+ {
861
+ action: import_zod2.z.enum(["arm", "wait"]).describe(
862
+ "'arm' = register handler and return immediately; 'wait' = await the previously armed handler"
863
+ ),
864
+ accept: import_zod2.z.boolean().optional().default(true).describe(
865
+ "Accept (true) or dismiss (false) the dialog. Only used with action='arm'."
866
+ ),
867
+ promptText: import_zod2.z.string().optional().describe(
868
+ "Text to enter if the dialog is a prompt. Only used with action='arm'."
869
+ ),
870
+ timeoutMs: import_zod2.z.number().optional().describe(
871
+ "Timeout in ms for 'wait' action. Default: 30000."
872
+ )
873
+ },
874
+ ({ action, accept, promptText, timeoutMs }) => this.withLock(async () => {
875
+ if (action === "arm") {
876
+ this.armedDialog = requirePage().armDialog({
877
+ accept: accept ?? true,
878
+ promptText,
879
+ timeoutMs
880
+ });
881
+ this.armedDialog.catch(() => {
882
+ });
883
+ return { content: [{ type: "text", text: "Dialog handler armed. Trigger the dialog now, then call browser_dialog with action='wait'." }] };
884
+ } else {
885
+ if (!this.armedDialog) {
886
+ return {
887
+ content: [{ type: "text", text: "No dialog handler is armed. Call browser_dialog with action='arm' first." }],
888
+ isError: true
889
+ };
890
+ }
891
+ const pending = this.armedDialog;
892
+ this.armedDialog = null;
893
+ await pending;
894
+ return { content: [{ type: "text", text: "Dialog handled successfully." }] };
895
+ }
412
896
  })
413
897
  );
414
898
  }
@@ -416,33 +900,33 @@ var BrowserTools = class {
416
900
 
417
901
  // src/tools/notebook.ts
418
902
  var import_zod3 = require("zod");
419
- var import_promises2 = __toESM(require("fs/promises"));
903
+ var import_promises3 = __toESM(require("fs/promises"));
420
904
  var import_child_process2 = require("child_process");
421
905
  var import_util2 = require("util");
422
906
  var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
423
907
  async function readNotebook(filePath) {
424
- const raw = await import_promises2.default.readFile(filePath, "utf-8");
908
+ const raw = await import_promises3.default.readFile(filePath, "utf-8");
425
909
  try {
426
910
  return JSON.parse(raw);
427
911
  } catch {
428
- throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 Jupyter \uB178\uD2B8\uBD81 \uD30C\uC77C\uC785\uB2C8\uB2E4: ${filePath}`);
912
+ throw new Error(`Invalid Jupyter notebook file: ${filePath}`);
429
913
  }
430
914
  }
431
915
  async function writeNotebook(filePath, nb) {
432
- await import_promises2.default.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
916
+ await import_promises3.default.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
433
917
  }
434
918
  var NotebookTools = class {
435
919
  register(server) {
436
920
  server.tool(
437
921
  "notebook_read",
438
- ".ipynb \uB178\uD2B8\uBD81 \uC77D\uAE30",
439
- { path: import_zod3.z.string().describe("\uB178\uD2B8\uBD81 \uD30C\uC77C \uACBD\uB85C") },
922
+ "Read .ipynb notebook",
923
+ { path: import_zod3.z.string().describe("Notebook file path") },
440
924
  async ({ path: filePath }) => {
441
925
  const nb = await readNotebook(filePath);
442
926
  const cells = nb.cells.map((cell, i) => ({
443
927
  index: i,
444
928
  type: cell.cell_type,
445
- source: cell.source.join(""),
929
+ source: Array.isArray(cell.source) ? cell.source.join("") : cell.source,
446
930
  outputs: cell.outputs?.length ?? 0
447
931
  }));
448
932
  return {
@@ -452,30 +936,30 @@ var NotebookTools = class {
452
936
  );
453
937
  server.tool(
454
938
  "notebook_edit_cell",
455
- "\uB178\uD2B8\uBD81 \uD2B9\uC815 \uC140 \uC218\uC815",
939
+ "Edit a specific notebook cell",
456
940
  {
457
941
  path: import_zod3.z.string(),
458
- cell_index: import_zod3.z.number().describe("0\uBD80\uD130 \uC2DC\uC791\uD558\uB294 \uC140 \uC778\uB371\uC2A4"),
459
- source: import_zod3.z.string().describe("\uC0C8 \uC18C\uC2A4 \uCF54\uB4DC")
942
+ cell_index: import_zod3.z.number().describe("Cell index (0-based)"),
943
+ source: import_zod3.z.string().describe("New source code")
460
944
  },
461
945
  async ({ path: filePath, cell_index, source }) => {
462
946
  const nb = await readNotebook(filePath);
463
947
  if (cell_index < 0 || cell_index >= nb.cells.length) {
464
- throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uC140 \uC778\uB371\uC2A4: ${cell_index}`);
948
+ throw new Error(`Invalid cell index: ${cell_index}`);
465
949
  }
466
950
  nb.cells[cell_index].source = source.split("\n").map(
467
951
  (l, i, arr) => i < arr.length - 1 ? l + "\n" : l
468
952
  );
469
953
  await writeNotebook(filePath, nb);
470
- return { content: [{ type: "text", text: "\uC140 \uC218\uC815 \uC644\uB8CC" }] };
954
+ return { content: [{ type: "text", text: "Cell updated" }] };
471
955
  }
472
956
  );
473
957
  server.tool(
474
958
  "notebook_execute",
475
- "\uB178\uD2B8\uBD81 \uC2E4\uD589 (nbconvert --execute)",
959
+ "Execute notebook (nbconvert --execute)",
476
960
  {
477
- path: import_zod3.z.string().describe("\uB178\uD2B8\uBD81 \uD30C\uC77C \uACBD\uB85C"),
478
- timeout: import_zod3.z.number().optional().default(300).describe("\uC140\uB2F9 \uD0C0\uC784\uC544\uC6C3 (\uCD08)")
961
+ path: import_zod3.z.string().describe("Notebook file path"),
962
+ timeout: import_zod3.z.number().optional().default(300).describe("Timeout per cell (seconds)")
479
963
  },
480
964
  async ({ path: filePath, timeout }) => {
481
965
  const nbconvertArgs = `nbconvert --to notebook --execute --inplace "${filePath}" --ExecutePreprocessor.timeout=${timeout}`;
@@ -491,7 +975,7 @@ var NotebookTools = class {
491
975
  for (const jupyter of candidates) {
492
976
  try {
493
977
  const { stdout, stderr } = await execAsync2(`${jupyter} ${nbconvertArgs}`);
494
- return { content: [{ type: "text", text: stdout || stderr || "\uC2E4\uD589 \uC644\uB8CC" }] };
978
+ return { content: [{ type: "text", text: stdout || stderr || "Execution complete" }] };
495
979
  } catch (err) {
496
980
  const error = err;
497
981
  if (error.code !== "127" && !error.message?.includes("not found") && !error.message?.includes("No such file")) {
@@ -499,17 +983,17 @@ var NotebookTools = class {
499
983
  }
500
984
  }
501
985
  }
502
- throw new Error("jupyter\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC124\uCE58 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694: pip install jupyter");
986
+ throw new Error("jupyter not found. Install it and try again: pip install jupyter");
503
987
  }
504
988
  );
505
989
  server.tool(
506
990
  "notebook_add_cell",
507
- "\uB178\uD2B8\uBD81\uC5D0 \uC0C8 \uC140 \uCD94\uAC00",
991
+ "Add a new cell to notebook",
508
992
  {
509
- path: import_zod3.z.string().describe(".ipynb \uD30C\uC77C \uACBD\uB85C"),
510
- cell_type: import_zod3.z.enum(["code", "markdown"]).describe("\uC140 \uD0C0\uC785"),
511
- source: import_zod3.z.string().describe("\uC140 \uC18C\uC2A4 \uB0B4\uC6A9"),
512
- position: import_zod3.z.number().optional().describe("\uC0BD\uC785 \uC704\uCE58(0-based). \uC5C6\uC73C\uBA74 \uB9E8 \uB05D\uC5D0 \uCD94\uAC00")
993
+ path: import_zod3.z.string().describe(".ipynb file path"),
994
+ cell_type: import_zod3.z.enum(["code", "markdown"]).describe("Cell type"),
995
+ source: import_zod3.z.string().describe("Cell source content"),
996
+ position: import_zod3.z.number().optional().describe("Insert position (0-based). Appends to end if omitted")
513
997
  },
514
998
  async ({ path: filePath, cell_type: cellType, source, position }) => {
515
999
  const nb = await readNotebook(filePath);
@@ -528,31 +1012,31 @@ var NotebookTools = class {
528
1012
  } else if (position > nb.cells.length) {
529
1013
  nb.cells.push(newCell);
530
1014
  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)`;
1015
+ warning = ` (warning: position ${position} exceeded range, appended at end (index: ${actualIndex}))`;
532
1016
  } else {
533
1017
  const clamped = Math.max(0, position);
534
1018
  nb.cells.splice(clamped, 0, newCell);
535
1019
  actualIndex = clamped;
536
1020
  }
537
1021
  await writeNotebook(filePath, nb);
538
- return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})${warning}` }] };
1022
+ return { content: [{ type: "text", text: `Cell added (index: ${actualIndex})${warning}` }] };
539
1023
  }
540
1024
  );
541
1025
  server.tool(
542
1026
  "notebook_delete_cell",
543
- "\uB178\uD2B8\uBD81 \uD2B9\uC815 \uC140 \uC0AD\uC81C",
1027
+ "Delete a specific notebook cell",
544
1028
  {
545
- path: import_zod3.z.string().describe(".ipynb \uD30C\uC77C \uACBD\uB85C"),
546
- cell_index: import_zod3.z.number().describe("\uC0AD\uC81C\uD560 \uC140 \uC778\uB371\uC2A4 (0-based)")
1029
+ path: import_zod3.z.string().describe(".ipynb file path"),
1030
+ cell_index: import_zod3.z.number().describe("Cell index to delete (0-based)")
547
1031
  },
548
1032
  async ({ path: filePath, cell_index }) => {
549
1033
  const nb = await readNotebook(filePath);
550
1034
  if (cell_index < 0 || cell_index >= nb.cells.length) {
551
- throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uC140 \uC778\uB371\uC2A4: ${cell_index}`);
1035
+ throw new Error(`Invalid cell index: ${cell_index}`);
552
1036
  }
553
1037
  nb.cells.splice(cell_index, 1);
554
1038
  await writeNotebook(filePath, nb);
555
- return { content: [{ type: "text", text: `\uC140 \uC0AD\uC81C \uC644\uB8CC (index: ${cell_index})` }] };
1039
+ return { content: [{ type: "text", text: `Cell deleted (index: ${cell_index})` }] };
556
1040
  }
557
1041
  );
558
1042
  }
@@ -572,42 +1056,9 @@ function platform() {
572
1056
  }
573
1057
  var DeviceTools = class {
574
1058
  register(server) {
575
- server.tool(
576
- "screen_capture",
577
- "\uD654\uBA74 \uC2A4\uD06C\uB9B0\uC0F7 (OS \uB124\uC774\uD2F0\uBE0C)",
578
- {
579
- output_path: import_zod4.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (\uC5C6\uC73C\uBA74 temp \uC800\uC7A5 \uD6C4 base64 \uBC18\uD658)")
580
- },
581
- async ({ output_path }) => {
582
- const p = platform();
583
- const isTmp = !output_path;
584
- const tmpPath = output_path ?? `/tmp/junis_screen_${Date.now()}.png`;
585
- const cmd = {
586
- mac: `screencapture -x "${tmpPath}"`,
587
- win: `nircmd.exe savescreenshot "${tmpPath}"`,
588
- linux: `scrot "${tmpPath}"`
589
- }[p];
590
- try {
591
- await execAsync3(cmd);
592
- } catch (err) {
593
- throw new Error(`\uD654\uBA74 \uCEA1\uCC98 \uC2E4\uD328: ${err.message}`);
594
- }
595
- const { readFileSync, unlinkSync } = await import("fs");
596
- const data = readFileSync(tmpPath).toString("base64");
597
- if (isTmp) {
598
- try {
599
- unlinkSync(tmpPath);
600
- } catch {
601
- }
602
- }
603
- return {
604
- content: [{ type: "image", data, mimeType: "image/png" }]
605
- };
606
- }
607
- );
608
1059
  server.tool(
609
1060
  "camera_capture",
610
- "\uCE74\uBA54\uB77C \uC0AC\uC9C4 \uCD2C\uC601",
1061
+ "Camera photo capture",
611
1062
  {
612
1063
  output_path: import_zod4.z.string().optional()
613
1064
  },
@@ -625,10 +1076,10 @@ var DeviceTools = class {
625
1076
  } catch (err) {
626
1077
  const e = err;
627
1078
  return {
628
- content: [{ type: "text", text: `\u274C \uCE74\uBA54\uB77C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uAC70\uB098 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
629
- \uC6D0\uC778: ${e.message}
1079
+ content: [{ type: "text", text: `\u274C Camera not found or inaccessible.
1080
+ Cause: ${e.message}
630
1081
 
631
- \uCE74\uBA54\uB77C\uAC00 \uC5F0\uACB0\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.` }],
1082
+ Please check if a camera is connected.` }],
632
1083
  isError: true
633
1084
  };
634
1085
  }
@@ -645,10 +1096,10 @@ var DeviceTools = class {
645
1096
  );
646
1097
  server.tool(
647
1098
  "notification_send",
648
- "OS \uC54C\uB9BC \uC804\uC1A1",
1099
+ "Send OS notification",
649
1100
  {
650
- title: import_zod4.z.string().describe("\uC54C\uB9BC \uC81C\uBAA9"),
651
- message: import_zod4.z.string().describe("\uC54C\uB9BC \uB0B4\uC6A9")
1101
+ title: import_zod4.z.string().describe("Notification title"),
1102
+ message: import_zod4.z.string().describe("Notification body")
652
1103
  },
653
1104
  async ({ title, message }) => {
654
1105
  try {
@@ -661,10 +1112,10 @@ var DeviceTools = class {
661
1112
  }
662
1113
  );
663
1114
  });
664
- return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
1115
+ return { content: [{ type: "text", text: "Notification sent" }] };
665
1116
  } catch (err) {
666
1117
  return {
667
- content: [{ type: "text", text: `\uC54C\uB9BC \uC804\uC1A1 \uC2E4\uD328: ${err.message}` }],
1118
+ content: [{ type: "text", text: `Notification failed: ${err.message}` }],
668
1119
  isError: true
669
1120
  };
670
1121
  }
@@ -672,7 +1123,7 @@ var DeviceTools = class {
672
1123
  );
673
1124
  server.tool(
674
1125
  "clipboard_read",
675
- "\uD074\uB9BD\uBCF4\uB4DC \uC77D\uAE30",
1126
+ "Read clipboard",
676
1127
  {},
677
1128
  async () => {
678
1129
  const p = platform();
@@ -683,7 +1134,7 @@ var DeviceTools = class {
683
1134
  );
684
1135
  server.tool(
685
1136
  "clipboard_write",
686
- "\uD074\uB9BD\uBCF4\uB4DC \uC4F0\uAE30",
1137
+ "Write to clipboard",
687
1138
  { text: import_zod4.z.string() },
688
1139
  async ({ text }) => {
689
1140
  const p = platform();
@@ -693,21 +1144,21 @@ var DeviceTools = class {
693
1144
  linux: `echo "${text}" | xclip -selection clipboard`
694
1145
  }[p];
695
1146
  await execAsync3(cmd);
696
- return { content: [{ type: "text", text: "\uD074\uB9BD\uBCF4\uB4DC \uC800\uC7A5 \uC644\uB8CC" }] };
1147
+ return { content: [{ type: "text", text: "Saved to clipboard" }] };
697
1148
  }
698
1149
  );
699
1150
  server.tool(
700
1151
  "screen_record",
701
- "\uD654\uBA74 \uB179\uD654 \uC2DC\uC791/\uC911\uC9C0 (macOS: screencapture -v, \uAE30\uD0C0: ffmpeg)",
1152
+ "Start/stop screen recording (macOS: screencapture -v, others: ffmpeg)",
702
1153
  {
703
- action: import_zod4.z.enum(["start", "stop"]).describe("start: \uB179\uD654 \uC2DC\uC791, stop: \uB179\uD654 \uC911\uC9C0"),
704
- output_path: import_zod4.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (start \uC2DC \uC0AC\uC6A9, \uAE30\uBCF8: /tmp/junis_record_<timestamp>.mp4)")
1154
+ action: import_zod4.z.enum(["start", "stop"]).describe("start: begin recording, stop: end recording"),
1155
+ output_path: import_zod4.z.string().optional().describe("Output path (used on start, default: /tmp/junis_record_<timestamp>.mp4)")
705
1156
  },
706
1157
  async ({ action, output_path }) => {
707
1158
  const p = platform();
708
1159
  if (action === "start") {
709
1160
  if (screenRecordPid) {
710
- return { content: [{ type: "text", text: "\uC774\uBBF8 \uB179\uD654 \uC911\uC785\uB2C8\uB2E4." }] };
1161
+ return { content: [{ type: "text", text: "Already recording." }] };
711
1162
  }
712
1163
  const tmpPath = output_path ?? `/tmp/junis_record_${Date.now()}.mp4`;
713
1164
  const { spawn } = await import("child_process");
@@ -715,10 +1166,10 @@ var DeviceTools = class {
715
1166
  const child = spawn(cmd[0], cmd[1], { detached: true, stdio: "ignore" });
716
1167
  child.unref();
717
1168
  screenRecordPid = child.pid ?? null;
718
- return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${screenRecordPid})` }] };
1169
+ return { content: [{ type: "text", text: `Recording started. Output path: ${tmpPath} (PID: ${screenRecordPid})` }] };
719
1170
  } else {
720
1171
  if (!screenRecordPid) {
721
- return { content: [{ type: "text", text: "\uD604\uC7AC \uB179\uD654 \uC911\uC774 \uC544\uB2D9\uB2C8\uB2E4." }] };
1172
+ return { content: [{ type: "text", text: "Not currently recording." }] };
722
1173
  }
723
1174
  try {
724
1175
  process.kill(screenRecordPid, "SIGINT");
@@ -726,13 +1177,13 @@ var DeviceTools = class {
726
1177
  } catch {
727
1178
  }
728
1179
  screenRecordPid = null;
729
- return { content: [{ type: "text", text: "\uB179\uD654 \uC911\uC9C0\uB428." }] };
1180
+ return { content: [{ type: "text", text: "Recording stopped." }] };
730
1181
  }
731
1182
  }
732
1183
  );
733
1184
  server.tool(
734
1185
  "location_get",
735
- "\uD604\uC7AC \uC704\uCE58 \uC870\uD68C (macOS: CoreLocation CLI, \uAE30\uD0C0: IP \uAE30\uBC18 fallback)",
1186
+ "Get current location (macOS: CoreLocation CLI, others: IP-based fallback)",
736
1187
  {},
737
1188
  async () => {
738
1189
  const p = platform();
@@ -740,28 +1191,28 @@ var DeviceTools = class {
740
1191
  try {
741
1192
  const { stdout } = await execAsync3("CoreLocationCLI -once -format '%latitude,%longitude'", { timeout: 1e4 });
742
1193
  const [lat, lon] = stdout.trim().split(",");
743
- return { content: [{ type: "text", text: `\uC704\uB3C4: ${lat}, \uACBD\uB3C4: ${lon}` }] };
1194
+ return { content: [{ type: "text", text: `Latitude: ${lat}, Longitude: ${lon}` }] };
744
1195
  } catch {
745
1196
  }
746
1197
  }
747
1198
  const res = await fetch("http://ip-api.com/json/");
748
1199
  const data = await res.json();
749
1200
  if (data.status !== "success") {
750
- throw new Error(`IP \uC704\uCE58 \uC870\uD68C \uC2E4\uD328: ${data.message ?? data.status}`);
1201
+ throw new Error(`IP location lookup failed: ${data.message ?? data.status}`);
751
1202
  }
752
1203
  return {
753
1204
  content: [{
754
1205
  type: "text",
755
- text: `\uC704\uB3C4: ${data.lat}, \uACBD\uB3C4: ${data.lon}, \uB3C4\uC2DC: ${data.city}, \uAD6D\uAC00: ${data.country} (IP \uAE30\uBC18 \uCD94\uC815)`
1206
+ text: `Latitude: ${data.lat}, Longitude: ${data.lon}, City: ${data.city}, Country: ${data.country} (estimated via IP)`
756
1207
  }]
757
1208
  };
758
1209
  }
759
1210
  );
760
1211
  server.tool(
761
1212
  "audio_play",
762
- "\uC624\uB514\uC624 \uD30C\uC77C \uC7AC\uC0DD (macOS: afplay, \uAE30\uD0C0: ffplay)",
1213
+ "Play audio file (macOS: afplay, others: ffplay)",
763
1214
  {
764
- file_path: import_zod4.z.string().describe("\uC7AC\uC0DD\uD560 \uC624\uB514\uC624 \uD30C\uC77C \uACBD\uB85C")
1215
+ file_path: import_zod4.z.string().describe("Path to the audio file to play")
765
1216
  },
766
1217
  async ({ file_path }) => {
767
1218
  const p = platform();
@@ -771,14 +1222,14 @@ var DeviceTools = class {
771
1222
  linux: `ffplay -nodisp -autoexit "${file_path}"`
772
1223
  }[p];
773
1224
  await execAsync3(cmd);
774
- return { content: [{ type: "text", text: `\uC7AC\uC0DD \uC644\uB8CC: ${file_path}` }] };
1225
+ return { content: [{ type: "text", text: `Playback complete: ${file_path}` }] };
775
1226
  }
776
1227
  );
777
1228
  }
778
1229
  };
779
1230
 
780
1231
  // src/server/stdio.ts
781
- async function main() {
1232
+ async function startStdioServer() {
782
1233
  const server = new import_mcp.McpServer({ name: "junis", version: "0.1.0" });
783
1234
  const fsTools = new FilesystemTools();
784
1235
  fsTools.register(server);
@@ -796,4 +1247,10 @@ async function main() {
796
1247
  process.exit(0);
797
1248
  });
798
1249
  }
799
- main().catch(console.error);
1250
+ if (require.main === module) {
1251
+ startStdioServer().catch(console.error);
1252
+ }
1253
+ // Annotate the CommonJS export names for ESM import in node:
1254
+ 0 && (module.exports = {
1255
+ startStdioServer
1256
+ });