junis 0.1.0

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.
@@ -0,0 +1,987 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli/index.ts
27
+ var import_commander = require("commander");
28
+
29
+ // src/cli/config.ts
30
+ var import_fs = __toESM(require("fs"));
31
+ var import_path = __toESM(require("path"));
32
+ var import_os = __toESM(require("os"));
33
+ var CONFIG_DIR = import_path.default.join(import_os.default.homedir(), ".junis");
34
+ var CONFIG_FILE = import_path.default.join(CONFIG_DIR, "config.json");
35
+ function loadConfig() {
36
+ try {
37
+ if (!import_fs.default.existsSync(CONFIG_FILE)) return null;
38
+ const raw = import_fs.default.readFileSync(CONFIG_FILE, "utf-8");
39
+ return JSON.parse(raw);
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+ function saveConfig(config) {
45
+ import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true });
46
+ import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
47
+ }
48
+ function clearConfig() {
49
+ if (import_fs.default.existsSync(CONFIG_FILE)) {
50
+ import_fs.default.unlinkSync(CONFIG_FILE);
51
+ }
52
+ }
53
+
54
+ // src/cli/auth.ts
55
+ var import_open = __toESM(require("open"));
56
+ var JUNIS_API = process.env.JUNIS_API_URL ?? "https://api.junis.ai";
57
+ var JUNIS_WEB = process.env.JUNIS_WEB_URL ?? "https://junis.ai";
58
+ async function authenticate() {
59
+ const startRes = await fetch(`${JUNIS_API}/api/auth/device/start`, {
60
+ method: "POST"
61
+ });
62
+ if (!startRes.ok) {
63
+ throw new Error(`Auth \uC2DC\uC791 \uC2E4\uD328: ${startRes.status}`);
64
+ }
65
+ const startData = await startRes.json();
66
+ const verificationUri = startData.verification_uri.replace(
67
+ /^https?:\/\/[^/]+/,
68
+ JUNIS_WEB
69
+ );
70
+ console.log("\n\u{1F310} \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uB85C\uADF8\uC778 \uD6C4 \uC2B9\uC778 \uBC84\uD2BC\uC744 \uD074\uB9AD\uD558\uC138\uC694.");
71
+ console.log(` URL: ${verificationUri}
72
+ `);
73
+ await (0, import_open.default)(verificationUri);
74
+ const deadline = Date.now() + startData.expires_in * 1e3;
75
+ const intervalMs = startData.interval * 1e3;
76
+ while (Date.now() < deadline) {
77
+ await sleep(intervalMs);
78
+ const pollRes = await fetch(
79
+ `${JUNIS_API}/api/auth/device/poll?code=${startData.device_code}`
80
+ );
81
+ if (pollRes.status === 202) {
82
+ process.stdout.write(".");
83
+ continue;
84
+ }
85
+ if (pollRes.status === 200) {
86
+ const result = await pollRes.json();
87
+ console.log("\n\u2705 \uC778\uC99D \uC644\uB8CC!\n");
88
+ return result;
89
+ }
90
+ if (pollRes.status === 410) {
91
+ throw new Error("\uC778\uC99D \uCF54\uB4DC\uAC00 \uB9CC\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2E4\uD589\uD574\uC8FC\uC138\uC694.");
92
+ }
93
+ throw new Error(`\uC608\uC0C1\uCE58 \uBABB\uD55C \uC751\uB2F5: ${pollRes.status}`);
94
+ }
95
+ throw new Error("\uC778\uC99D \uC2DC\uAC04 \uCD08\uACFC (5\uBD84). \uB2E4\uC2DC \uC2E4\uD589\uD574\uC8FC\uC138\uC694.");
96
+ }
97
+ function sleep(ms) {
98
+ return new Promise((resolve) => setTimeout(resolve, ms));
99
+ }
100
+
101
+ // src/relay/client.ts
102
+ var import_ws = __toESM(require("ws"));
103
+ var JUNIS_WS = process.env.JUNIS_WS_URL ?? "wss://api.junis.ai";
104
+ var RelayClient = class {
105
+ constructor(config, onMCPRequest) {
106
+ this.config = config;
107
+ this.onMCPRequest = onMCPRequest;
108
+ }
109
+ ws = null;
110
+ reconnectDelay = 1e3;
111
+ heartbeatTimer = null;
112
+ destroyed = false;
113
+ async connect() {
114
+ if (this.destroyed) return;
115
+ const url = `${JUNIS_WS}/api/ws/devices/${this.config.device_key}?token=${this.config.token}`;
116
+ console.log(`\u{1F517} \uB9B4\uB808\uC774 \uC11C\uBC84 \uC5F0\uACB0 \uC911...`);
117
+ this.ws = new import_ws.default(url);
118
+ this.ws.on("open", () => {
119
+ console.log("\u2705 \uB9B4\uB808\uC774 \uC11C\uBC84 \uC5F0\uACB0\uB428");
120
+ this.reconnectDelay = 1e3;
121
+ this.startHeartbeat();
122
+ });
123
+ this.ws.on("message", async (raw) => {
124
+ try {
125
+ const msg = JSON.parse(raw.toString());
126
+ if (msg.type === "pong") return;
127
+ if (msg.type === "mcp_request") {
128
+ try {
129
+ const result = await this.onMCPRequest(msg.id, msg.payload);
130
+ this.send({ type: "mcp_response", id: msg.id, payload: result });
131
+ } catch (err) {
132
+ this.send({
133
+ type: "mcp_response",
134
+ id: msg.id,
135
+ payload: { error: String(err) }
136
+ });
137
+ }
138
+ }
139
+ } catch {
140
+ }
141
+ });
142
+ this.ws.on("close", () => {
143
+ this.stopHeartbeat();
144
+ if (!this.destroyed) {
145
+ console.log(`\u26A0\uFE0F \uC5F0\uACB0 \uB04A\uAE40. ${this.reconnectDelay / 1e3}\uCD08 \uD6C4 \uC7AC\uC811\uC18D...`);
146
+ setTimeout(() => this.connect(), this.reconnectDelay);
147
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, 3e4);
148
+ }
149
+ });
150
+ this.ws.on("error", (err) => {
151
+ console.error(`\uB9B4\uB808\uC774 \uC624\uB958: ${err.message}`);
152
+ });
153
+ }
154
+ send(data) {
155
+ if (this.ws?.readyState === import_ws.default.OPEN) {
156
+ this.ws.send(JSON.stringify(data));
157
+ }
158
+ }
159
+ startHeartbeat() {
160
+ this.heartbeatTimer = setInterval(() => {
161
+ this.send({ type: "heartbeat" });
162
+ }, 3e4);
163
+ }
164
+ stopHeartbeat() {
165
+ if (this.heartbeatTimer) {
166
+ clearInterval(this.heartbeatTimer);
167
+ this.heartbeatTimer = null;
168
+ }
169
+ }
170
+ destroy() {
171
+ this.destroyed = true;
172
+ this.stopHeartbeat();
173
+ this.ws?.close();
174
+ }
175
+ };
176
+
177
+ // src/server/mcp.ts
178
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
179
+ var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
180
+ var import_http = require("http");
181
+
182
+ // src/tools/filesystem.ts
183
+ var import_child_process = require("child_process");
184
+ var import_util = require("util");
185
+ var import_promises = __toESM(require("fs/promises"));
186
+ var import_path2 = __toESM(require("path"));
187
+ var import_glob = require("glob");
188
+ var import_zod = require("zod");
189
+ var execAsync = (0, import_util.promisify)(import_child_process.exec);
190
+ var FilesystemTools = class {
191
+ register(server) {
192
+ server.tool(
193
+ "execute_command",
194
+ "\uD130\uBBF8\uB110 \uBA85\uB839 \uC2E4\uD589",
195
+ {
196
+ command: import_zod.z.string().describe("\uC2E4\uD589\uD560 \uC258 \uBA85\uB839"),
197
+ timeout_ms: import_zod.z.number().optional().default(3e4).describe("\uD0C0\uC784\uC544\uC6C3 (ms)"),
198
+ background: import_zod.z.boolean().optional().default(false).describe("\uBC31\uADF8\uB77C\uC6B4\uB4DC \uC2E4\uD589")
199
+ },
200
+ async ({ command, timeout_ms, background }) => {
201
+ if (background) {
202
+ (0, import_child_process.exec)(command);
203
+ return { content: [{ type: "text", text: "\uBC31\uADF8\uB77C\uC6B4\uB4DC \uC2E4\uD589 \uC2DC\uC791\uB428" }] };
204
+ }
205
+ try {
206
+ const { stdout, stderr } = await execAsync(command, {
207
+ timeout: timeout_ms
208
+ });
209
+ return {
210
+ content: [{ type: "text", text: stdout || stderr || "(\uCD9C\uB825 \uC5C6\uC74C)" }]
211
+ };
212
+ } catch (err) {
213
+ const error = err;
214
+ return {
215
+ content: [
216
+ {
217
+ type: "text",
218
+ text: `\uC624\uB958 (exit ${error.code ?? "?"}): ${error.message}
219
+ ${error.stderr ?? ""}`
220
+ }
221
+ ],
222
+ isError: true
223
+ };
224
+ }
225
+ }
226
+ );
227
+ server.tool(
228
+ "read_file",
229
+ "\uD30C\uC77C \uC77D\uAE30",
230
+ {
231
+ path: import_zod.z.string().describe("\uD30C\uC77C \uACBD\uB85C"),
232
+ encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("\uC778\uCF54\uB529")
233
+ },
234
+ async ({ path: filePath, encoding }) => {
235
+ const content = await import_promises.default.readFile(filePath, encoding);
236
+ return { content: [{ type: "text", text: content }] };
237
+ }
238
+ );
239
+ server.tool(
240
+ "write_file",
241
+ "\uD30C\uC77C \uC4F0\uAE30/\uC0DD\uC131",
242
+ {
243
+ path: import_zod.z.string().describe("\uD30C\uC77C \uACBD\uB85C"),
244
+ content: import_zod.z.string().describe("\uD30C\uC77C \uB0B4\uC6A9")
245
+ },
246
+ async ({ path: filePath, content }) => {
247
+ await import_promises.default.mkdir(import_path2.default.dirname(filePath), { recursive: true });
248
+ await import_promises.default.writeFile(filePath, content, "utf-8");
249
+ return { content: [{ type: "text", text: "\uD30C\uC77C \uC800\uC7A5 \uC644\uB8CC" }] };
250
+ }
251
+ );
252
+ server.tool(
253
+ "list_directory",
254
+ "\uB514\uB809\uD1A0\uB9AC \uBAA9\uB85D \uC870\uD68C",
255
+ {
256
+ path: import_zod.z.string().describe("\uB514\uB809\uD1A0\uB9AC \uACBD\uB85C")
257
+ },
258
+ async ({ path: dirPath }) => {
259
+ const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true });
260
+ const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
261
+ return { content: [{ type: "text", text: lines.join("\n") }] };
262
+ }
263
+ );
264
+ server.tool(
265
+ "search_code",
266
+ "\uCF54\uB4DC/\uD14D\uC2A4\uD2B8 \uAC80\uC0C9",
267
+ {
268
+ pattern: import_zod.z.string().describe("\uAC80\uC0C9 \uD328\uD134 (\uC815\uADDC\uC2DD \uC9C0\uC6D0)"),
269
+ directory: import_zod.z.string().optional().default(".").describe("\uAC80\uC0C9 \uB514\uB809\uD1A0\uB9AC"),
270
+ file_pattern: import_zod.z.string().optional().default("**/*").describe("\uD30C\uC77C \uD328\uD134")
271
+ },
272
+ async ({ pattern, directory, file_pattern }) => {
273
+ try {
274
+ const { stdout } = await execAsync(
275
+ `rg --no-heading -n "${pattern}" ${directory}`,
276
+ { timeout: 1e4 }
277
+ );
278
+ return { content: [{ type: "text", text: stdout || "\uACB0\uACFC \uC5C6\uC74C" }] };
279
+ } catch {
280
+ const files = await (0, import_glob.glob)(file_pattern, { cwd: directory });
281
+ const results = [];
282
+ for (const file of files.slice(0, 100)) {
283
+ try {
284
+ const content = await import_promises.default.readFile(
285
+ import_path2.default.join(directory, file),
286
+ "utf-8"
287
+ );
288
+ const lines = content.split("\n");
289
+ const re = new RegExp(pattern, "gi");
290
+ lines.forEach((line, i) => {
291
+ if (re.test(line)) results.push(`${file}:${i + 1}: ${line}`);
292
+ });
293
+ } catch {
294
+ }
295
+ }
296
+ return {
297
+ content: [
298
+ { type: "text", text: results.join("\n") || "\uACB0\uACFC \uC5C6\uC74C" }
299
+ ]
300
+ };
301
+ }
302
+ }
303
+ );
304
+ server.tool(
305
+ "list_processes",
306
+ "\uC2E4\uD589 \uC911\uC778 \uD504\uB85C\uC138\uC2A4 \uBAA9\uB85D",
307
+ {},
308
+ async () => {
309
+ const cmd = process.platform === "win32" ? "tasklist" : process.platform === "darwin" ? "ps aux | sort -rk 3 | head -30" : "ps aux --sort=-%cpu | head -30";
310
+ const { stdout } = await execAsync(cmd);
311
+ return { content: [{ type: "text", text: stdout }] };
312
+ }
313
+ );
314
+ server.tool(
315
+ "kill_process",
316
+ "\uD504\uB85C\uC138\uC2A4 \uC885\uB8CC (SIGTERM \uD6C4 3\uCD08 \uB300\uAE30, \uC0B4\uC544\uC788\uC73C\uBA74 SIGKILL \uC790\uB3D9 \uC801\uC6A9)",
317
+ {
318
+ pid: import_zod.z.number().describe("\uC885\uB8CC\uD560 \uD504\uB85C\uC138\uC2A4 PID"),
319
+ 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)")
320
+ },
321
+ async ({ pid, signal }) => {
322
+ const isWindows = process.platform === "win32";
323
+ if (isWindows) {
324
+ await execAsync(`taskkill /PID ${pid} /F`);
325
+ return {
326
+ content: [{ type: "text", text: `PID ${pid} \uC885\uB8CC \uC644\uB8CC (taskkill /F)` }]
327
+ };
328
+ }
329
+ if (signal === "SIGKILL") {
330
+ await execAsync(`kill -9 ${pid}`);
331
+ return {
332
+ content: [{ type: "text", text: `PID ${pid} \uAC15\uC81C \uC885\uB8CC \uC644\uB8CC (SIGKILL)` }]
333
+ };
334
+ }
335
+ try {
336
+ await execAsync(`kill -15 ${pid}`);
337
+ } catch {
338
+ return {
339
+ content: [
340
+ { 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.` }
341
+ ],
342
+ isError: true
343
+ };
344
+ }
345
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
346
+ const isAlive = await execAsync(`kill -0 ${pid}`).then(() => true).catch(() => false);
347
+ if (!isAlive) {
348
+ return {
349
+ content: [{ type: "text", text: `PID ${pid} \uC885\uB8CC \uC644\uB8CC (SIGTERM)` }]
350
+ };
351
+ }
352
+ await execAsync(`kill -9 ${pid}`);
353
+ return {
354
+ content: [
355
+ {
356
+ type: "text",
357
+ text: `PID ${pid} \uAC15\uC81C \uC885\uB8CC \uC644\uB8CC (SIGTERM \uBB34\uC751\uB2F5 \u2192 SIGKILL \uC790\uB3D9 \uC801\uC6A9)`
358
+ }
359
+ ]
360
+ };
361
+ }
362
+ );
363
+ }
364
+ };
365
+
366
+ // src/tools/browser.ts
367
+ var import_playwright = require("playwright");
368
+ var import_zod2 = require("zod");
369
+ var BrowserTools = class {
370
+ browser = null;
371
+ page = null;
372
+ async init() {
373
+ try {
374
+ this.browser = await import_playwright.chromium.launch({ headless: true });
375
+ this.page = await this.browser.newPage();
376
+ } catch {
377
+ console.warn(
378
+ "\u26A0\uFE0F Playwright \uBBF8\uC124\uCE58. \uBE0C\uB77C\uC6B0\uC800 \uB3C4\uAD6C \uBE44\uD65C\uC131\uD654.\n \uD65C\uC131\uD654: npx playwright install chromium"
379
+ );
380
+ }
381
+ }
382
+ async cleanup() {
383
+ await this.browser?.close();
384
+ }
385
+ register(server) {
386
+ const requirePage = () => {
387
+ if (!this.page) throw new Error("\uBE0C\uB77C\uC6B0\uC800 \uBBF8\uCD08\uAE30\uD654. playwright \uC124\uCE58 \uD655\uC778.");
388
+ return this.page;
389
+ };
390
+ server.tool(
391
+ "browser_navigate",
392
+ "URL\uB85C \uC774\uB3D9",
393
+ { url: import_zod2.z.string().describe("\uC774\uB3D9\uD560 URL") },
394
+ async ({ url }) => {
395
+ const page = requirePage();
396
+ await page.goto(url, { waitUntil: "domcontentloaded" });
397
+ return {
398
+ content: [{ type: "text", text: `\uC774\uB3D9 \uC644\uB8CC: ${page.url()}` }]
399
+ };
400
+ }
401
+ );
402
+ server.tool(
403
+ "browser_click",
404
+ "\uC694\uC18C \uD074\uB9AD",
405
+ { selector: import_zod2.z.string().describe("CSS \uC120\uD0DD\uC790") },
406
+ async ({ selector }) => {
407
+ await requirePage().click(selector);
408
+ return { content: [{ type: "text", text: "\uD074\uB9AD \uC644\uB8CC" }] };
409
+ }
410
+ );
411
+ server.tool(
412
+ "browser_type",
413
+ "\uD14D\uC2A4\uD2B8 \uC785\uB825",
414
+ {
415
+ selector: import_zod2.z.string().describe("CSS \uC120\uD0DD\uC790"),
416
+ text: import_zod2.z.string().describe("\uC785\uB825\uD560 \uD14D\uC2A4\uD2B8"),
417
+ clear: import_zod2.z.boolean().optional().default(false).describe("\uAE30\uC874 \uB0B4\uC6A9 \uC0AD\uC81C \uD6C4 \uC785\uB825")
418
+ },
419
+ async ({ selector, text, clear }) => {
420
+ const page = requirePage();
421
+ if (clear) await page.fill(selector, text);
422
+ else await page.type(selector, text);
423
+ return { content: [{ type: "text", text: "\uC785\uB825 \uC644\uB8CC" }] };
424
+ }
425
+ );
426
+ server.tool(
427
+ "browser_screenshot",
428
+ "\uD604\uC7AC \uD398\uC774\uC9C0 \uC2A4\uD06C\uB9B0\uC0F7",
429
+ {
430
+ path: import_zod2.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (\uC5C6\uC73C\uBA74 base64 \uBC18\uD658)"),
431
+ full_page: import_zod2.z.boolean().optional().default(false)
432
+ },
433
+ async ({ path: path3, full_page }) => {
434
+ const page = requirePage();
435
+ const screenshot = await page.screenshot({
436
+ path: path3 ?? void 0,
437
+ fullPage: full_page
438
+ });
439
+ if (path3) {
440
+ return { content: [{ type: "text", text: `\uC800\uC7A5 \uC644\uB8CC: ${path3}` }] };
441
+ }
442
+ return {
443
+ content: [
444
+ {
445
+ type: "image",
446
+ data: screenshot.toString("base64"),
447
+ mimeType: "image/png"
448
+ }
449
+ ]
450
+ };
451
+ }
452
+ );
453
+ server.tool(
454
+ "browser_snapshot",
455
+ "\uD398\uC774\uC9C0 \uC811\uADFC\uC131 \uD2B8\uB9AC \uC870\uD68C (\uAD6C\uC870 \uD30C\uC545\uC6A9)",
456
+ {},
457
+ async () => {
458
+ const page = requirePage();
459
+ const snapshot = await page.locator("body").ariaSnapshot();
460
+ return {
461
+ content: [
462
+ { type: "text", text: snapshot }
463
+ ]
464
+ };
465
+ }
466
+ );
467
+ server.tool(
468
+ "browser_evaluate",
469
+ "JavaScript \uC2E4\uD589",
470
+ { code: import_zod2.z.string().describe("\uC2E4\uD589\uD560 JavaScript \uCF54\uB4DC") },
471
+ async ({ code }) => {
472
+ const result = await requirePage().evaluate(code);
473
+ return {
474
+ content: [
475
+ { type: "text", text: JSON.stringify(result, null, 2) }
476
+ ]
477
+ };
478
+ }
479
+ );
480
+ server.tool(
481
+ "browser_pdf",
482
+ "\uD604\uC7AC \uD398\uC774\uC9C0 PDF \uC800\uC7A5",
483
+ { path: import_zod2.z.string().describe("\uC800\uC7A5 \uACBD\uB85C (.pdf)") },
484
+ async ({ path: path3 }) => {
485
+ await requirePage().pdf({ path: path3 });
486
+ return { content: [{ type: "text", text: `PDF \uC800\uC7A5 \uC644\uB8CC: ${path3}` }] };
487
+ }
488
+ );
489
+ }
490
+ };
491
+
492
+ // src/tools/notebook.ts
493
+ var import_zod3 = require("zod");
494
+ var import_promises2 = __toESM(require("fs/promises"));
495
+ var import_child_process2 = require("child_process");
496
+ var import_util2 = require("util");
497
+ var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
498
+ async function readNotebook(filePath) {
499
+ const raw = await import_promises2.default.readFile(filePath, "utf-8");
500
+ return JSON.parse(raw);
501
+ }
502
+ async function writeNotebook(filePath, nb) {
503
+ await import_promises2.default.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
504
+ }
505
+ var NotebookTools = class {
506
+ register(server) {
507
+ server.tool(
508
+ "notebook_read",
509
+ ".ipynb \uB178\uD2B8\uBD81 \uC77D\uAE30",
510
+ { path: import_zod3.z.string().describe("\uB178\uD2B8\uBD81 \uD30C\uC77C \uACBD\uB85C") },
511
+ async ({ path: filePath }) => {
512
+ const nb = await readNotebook(filePath);
513
+ const cells = nb.cells.map((cell, i) => ({
514
+ index: i,
515
+ type: cell.cell_type,
516
+ source: cell.source.join(""),
517
+ outputs: cell.outputs?.length ?? 0
518
+ }));
519
+ return {
520
+ content: [{ type: "text", text: JSON.stringify(cells, null, 2) }]
521
+ };
522
+ }
523
+ );
524
+ server.tool(
525
+ "notebook_edit_cell",
526
+ "\uB178\uD2B8\uBD81 \uD2B9\uC815 \uC140 \uC218\uC815",
527
+ {
528
+ path: import_zod3.z.string(),
529
+ cell_index: import_zod3.z.number().describe("0\uBD80\uD130 \uC2DC\uC791\uD558\uB294 \uC140 \uC778\uB371\uC2A4"),
530
+ source: import_zod3.z.string().describe("\uC0C8 \uC18C\uC2A4 \uCF54\uB4DC")
531
+ },
532
+ async ({ path: filePath, cell_index, source }) => {
533
+ const nb = await readNotebook(filePath);
534
+ if (cell_index < 0 || cell_index >= nb.cells.length) {
535
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uC140 \uC778\uB371\uC2A4: ${cell_index}`);
536
+ }
537
+ nb.cells[cell_index].source = source.split("\n").map(
538
+ (l, i, arr) => i < arr.length - 1 ? l + "\n" : l
539
+ );
540
+ await writeNotebook(filePath, nb);
541
+ return { content: [{ type: "text", text: "\uC140 \uC218\uC815 \uC644\uB8CC" }] };
542
+ }
543
+ );
544
+ server.tool(
545
+ "notebook_execute",
546
+ "\uB178\uD2B8\uBD81 \uC2E4\uD589 (nbconvert --execute)",
547
+ {
548
+ path: import_zod3.z.string().describe("\uB178\uD2B8\uBD81 \uD30C\uC77C \uACBD\uB85C"),
549
+ timeout: import_zod3.z.number().optional().default(300).describe("\uC140\uB2F9 \uD0C0\uC784\uC544\uC6C3 (\uCD08)")
550
+ },
551
+ async ({ path: filePath, timeout }) => {
552
+ const nbconvertArgs = `nbconvert --to notebook --execute --inplace "${filePath}" --ExecutePreprocessor.timeout=${timeout}`;
553
+ const candidates = [
554
+ "jupyter",
555
+ `${process.env.HOME}/Library/Python/3.9/bin/jupyter`,
556
+ `${process.env.HOME}/Library/Python/3.10/bin/jupyter`,
557
+ `${process.env.HOME}/Library/Python/3.11/bin/jupyter`,
558
+ `${process.env.HOME}/Library/Python/3.12/bin/jupyter`,
559
+ "/usr/local/bin/jupyter",
560
+ "/opt/homebrew/bin/jupyter"
561
+ ];
562
+ for (const jupyter of candidates) {
563
+ try {
564
+ const { stdout, stderr } = await execAsync2(`${jupyter} ${nbconvertArgs}`);
565
+ return { content: [{ type: "text", text: stdout || stderr || "\uC2E4\uD589 \uC644\uB8CC" }] };
566
+ } catch (err) {
567
+ const error = err;
568
+ if (error.code !== "127" && !error.message?.includes("not found") && !error.message?.includes("No such file")) {
569
+ throw err;
570
+ }
571
+ }
572
+ }
573
+ 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");
574
+ }
575
+ );
576
+ }
577
+ };
578
+
579
+ // src/tools/device.ts
580
+ var import_child_process3 = require("child_process");
581
+ var import_util3 = require("util");
582
+ var import_zod4 = require("zod");
583
+ var execAsync3 = (0, import_util3.promisify)(import_child_process3.exec);
584
+ function platform() {
585
+ if (process.platform === "darwin") return "mac";
586
+ if (process.platform === "win32") return "win";
587
+ return "linux";
588
+ }
589
+ var DeviceTools = class {
590
+ register(server) {
591
+ server.tool(
592
+ "screen_capture",
593
+ "\uD654\uBA74 \uC2A4\uD06C\uB9B0\uC0F7 (OS \uB124\uC774\uD2F0\uBE0C)",
594
+ {
595
+ output_path: import_zod4.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (\uC5C6\uC73C\uBA74 temp \uC800\uC7A5 \uD6C4 base64 \uBC18\uD658)")
596
+ },
597
+ async ({ output_path }) => {
598
+ const p = platform();
599
+ const tmpPath = output_path ?? `/tmp/junis_screen_${Date.now()}.png`;
600
+ const cmd = {
601
+ mac: `screencapture -x "${tmpPath}"`,
602
+ win: `nircmd.exe savescreenshot "${tmpPath}"`,
603
+ linux: `scrot "${tmpPath}"`
604
+ }[p];
605
+ await execAsync3(cmd);
606
+ const { readFileSync } = await import("fs");
607
+ const data = readFileSync(tmpPath).toString("base64");
608
+ return {
609
+ content: [{ type: "image", data, mimeType: "image/png" }]
610
+ };
611
+ }
612
+ );
613
+ server.tool(
614
+ "camera_capture",
615
+ "\uCE74\uBA54\uB77C \uC0AC\uC9C4 \uCD2C\uC601",
616
+ {
617
+ output_path: import_zod4.z.string().optional()
618
+ },
619
+ async ({ output_path }) => {
620
+ const p = platform();
621
+ const tmpPath = output_path ?? `/tmp/junis_cam_${Date.now()}.jpg`;
622
+ const cmd = {
623
+ mac: `imagesnap "${tmpPath}"`,
624
+ win: `ffmpeg -f dshow -i video="Default" -frames:v 1 "${tmpPath}"`,
625
+ linux: `fswebcam -r 1280x720 "${tmpPath}"`
626
+ }[p];
627
+ await execAsync3(cmd);
628
+ const { readFileSync } = await import("fs");
629
+ const data = readFileSync(tmpPath).toString("base64");
630
+ return {
631
+ content: [{ type: "image", data, mimeType: "image/jpeg" }]
632
+ };
633
+ }
634
+ );
635
+ server.tool(
636
+ "notification_send",
637
+ "OS \uC54C\uB9BC \uC804\uC1A1",
638
+ {
639
+ title: import_zod4.z.string().describe("\uC54C\uB9BC \uC81C\uBAA9"),
640
+ message: import_zod4.z.string().describe("\uC54C\uB9BC \uB0B4\uC6A9")
641
+ },
642
+ async ({ title, message }) => {
643
+ const p = platform();
644
+ const cmd = {
645
+ mac: `osascript -e 'display notification "${message}" with title "${title}"'`,
646
+ 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())"`,
647
+ linux: `notify-send "${title}" "${message}"`
648
+ }[p];
649
+ await execAsync3(cmd);
650
+ return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
651
+ }
652
+ );
653
+ server.tool(
654
+ "clipboard_read",
655
+ "\uD074\uB9BD\uBCF4\uB4DC \uC77D\uAE30",
656
+ {},
657
+ async () => {
658
+ const p = platform();
659
+ const cmd = { mac: "pbpaste", win: "powershell Get-Clipboard", linux: "xclip -o" }[p];
660
+ const { stdout } = await execAsync3(cmd);
661
+ return { content: [{ type: "text", text: stdout }] };
662
+ }
663
+ );
664
+ server.tool(
665
+ "clipboard_write",
666
+ "\uD074\uB9BD\uBCF4\uB4DC \uC4F0\uAE30",
667
+ { text: import_zod4.z.string() },
668
+ async ({ text }) => {
669
+ const p = platform();
670
+ const cmd = {
671
+ mac: `echo "${text}" | pbcopy`,
672
+ win: `powershell Set-Clipboard "${text}"`,
673
+ linux: `echo "${text}" | xclip -selection clipboard`
674
+ }[p];
675
+ await execAsync3(cmd);
676
+ return { content: [{ type: "text", text: "\uD074\uB9BD\uBCF4\uB4DC \uC800\uC7A5 \uC644\uB8CC" }] };
677
+ }
678
+ );
679
+ }
680
+ };
681
+
682
+ // src/server/mcp.ts
683
+ var mcpPort = 3e3;
684
+ var globalBrowserTools = null;
685
+ function createMcpServer() {
686
+ const server = new import_mcp.McpServer({
687
+ name: "junis",
688
+ version: "0.1.0"
689
+ });
690
+ const fsTools = new FilesystemTools();
691
+ fsTools.register(server);
692
+ if (globalBrowserTools) {
693
+ globalBrowserTools.register(server);
694
+ }
695
+ const notebookTools = new NotebookTools();
696
+ notebookTools.register(server);
697
+ const deviceTools = new DeviceTools();
698
+ deviceTools.register(server);
699
+ return server;
700
+ }
701
+ function readBody(req) {
702
+ return new Promise((resolve, reject) => {
703
+ const chunks = [];
704
+ req.on("data", (chunk) => chunks.push(chunk));
705
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
706
+ req.on("error", reject);
707
+ });
708
+ }
709
+ function tryListen(httpServer, port, maxRetries) {
710
+ return new Promise((resolve, reject) => {
711
+ let currentPort = port;
712
+ let attempts = 0;
713
+ const attempt = () => {
714
+ httpServer.listen(currentPort, () => {
715
+ resolve(currentPort);
716
+ });
717
+ httpServer.once("error", (err) => {
718
+ if (err.code === "EADDRINUSE" && attempts < maxRetries) {
719
+ attempts++;
720
+ currentPort++;
721
+ httpServer.removeAllListeners("error");
722
+ attempt();
723
+ } else {
724
+ reject(err);
725
+ }
726
+ });
727
+ };
728
+ attempt();
729
+ });
730
+ }
731
+ function handleCors(res) {
732
+ res.writeHead(204, {
733
+ "Access-Control-Allow-Origin": "*",
734
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
735
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, MCP-Protocol-Version, mcp-protocol-version, MCP-Session-Id"
736
+ });
737
+ res.end();
738
+ }
739
+ function handleOAuthDiscovery(req, res, port) {
740
+ const url = req.url ?? "";
741
+ const baseUrl = `http://localhost:${port}`;
742
+ if (url === "/.well-known/oauth-protected-resource" || url === "/.well-known/oauth-protected-resource/mcp") {
743
+ const metadata = {
744
+ resource: `${baseUrl}/mcp`,
745
+ // authorization_servers 필드 자체를 생략 → 클라이언트가 AS를 찾지 않음
746
+ bearer_methods_supported: []
747
+ };
748
+ res.writeHead(200, {
749
+ "Content-Type": "application/json",
750
+ "Access-Control-Allow-Origin": "*"
751
+ });
752
+ res.end(JSON.stringify(metadata));
753
+ return true;
754
+ }
755
+ if (url === "/.well-known/oauth-authorization-server") {
756
+ const metadata = {
757
+ issuer: baseUrl,
758
+ authorization_endpoint: `${baseUrl}/oauth/authorize`,
759
+ token_endpoint: `${baseUrl}/oauth/token`,
760
+ registration_endpoint: `${baseUrl}/oauth/register`,
761
+ response_types_supported: ["code"],
762
+ grant_types_supported: ["authorization_code"],
763
+ code_challenge_methods_supported: ["S256"],
764
+ token_endpoint_auth_methods_supported: ["none"]
765
+ };
766
+ res.writeHead(200, {
767
+ "Content-Type": "application/json",
768
+ "Access-Control-Allow-Origin": "*"
769
+ });
770
+ res.end(JSON.stringify(metadata));
771
+ return true;
772
+ }
773
+ if (url === "/oauth/register" && req.method === "POST") {
774
+ const clientInfo = {
775
+ client_id: `local-${Date.now()}`,
776
+ client_id_issued_at: Math.floor(Date.now() / 1e3),
777
+ redirect_uris: [],
778
+ grant_types: ["authorization_code"],
779
+ response_types: ["code"],
780
+ token_endpoint_auth_method: "none"
781
+ };
782
+ res.writeHead(201, {
783
+ "Content-Type": "application/json",
784
+ "Access-Control-Allow-Origin": "*"
785
+ });
786
+ res.end(JSON.stringify(clientInfo));
787
+ return true;
788
+ }
789
+ if (url === "/oauth/token" && req.method === "POST") {
790
+ const tokenResponse = {
791
+ access_token: `local-token-${Date.now()}`,
792
+ token_type: "Bearer",
793
+ expires_in: 86400
794
+ };
795
+ res.writeHead(200, {
796
+ "Content-Type": "application/json",
797
+ "Access-Control-Allow-Origin": "*"
798
+ });
799
+ res.end(JSON.stringify(tokenResponse));
800
+ return true;
801
+ }
802
+ return false;
803
+ }
804
+ async function startMCPServer(port) {
805
+ globalBrowserTools = new BrowserTools();
806
+ await globalBrowserTools.init();
807
+ let resolvedPort = port;
808
+ const httpServer = (0, import_http.createServer)(
809
+ async (req, res) => {
810
+ try {
811
+ if (req.method === "OPTIONS") {
812
+ handleCors(res);
813
+ return;
814
+ }
815
+ if (handleOAuthDiscovery(req, res, resolvedPort)) {
816
+ return;
817
+ }
818
+ const url = req.url ?? "";
819
+ if (url === "/mcp") {
820
+ if (req.method === "POST") {
821
+ const mcpServer = createMcpServer();
822
+ const transport = new import_streamableHttp.StreamableHTTPServerTransport({
823
+ sessionIdGenerator: void 0
824
+ // stateless
825
+ });
826
+ try {
827
+ const rawBody = await readBody(req);
828
+ const parsedBody = rawBody ? JSON.parse(rawBody) : void 0;
829
+ await mcpServer.connect(transport);
830
+ await transport.handleRequest(req, res, parsedBody);
831
+ res.on("close", () => {
832
+ transport.close().catch(() => {
833
+ });
834
+ mcpServer.close().catch(() => {
835
+ });
836
+ });
837
+ } catch (err) {
838
+ console.error("MCP request error:", err);
839
+ if (!res.headersSent) {
840
+ res.writeHead(500, { "Content-Type": "application/json" });
841
+ res.end(
842
+ JSON.stringify({
843
+ jsonrpc: "2.0",
844
+ error: { code: -32603, message: "Internal server error" },
845
+ id: null
846
+ })
847
+ );
848
+ }
849
+ }
850
+ } else if (req.method === "GET") {
851
+ res.writeHead(405, { "Content-Type": "application/json" });
852
+ res.end(
853
+ JSON.stringify({
854
+ jsonrpc: "2.0",
855
+ error: { code: -32e3, message: "Method not allowed." },
856
+ id: null
857
+ })
858
+ );
859
+ } else if (req.method === "DELETE") {
860
+ res.writeHead(405, { "Content-Type": "application/json" });
861
+ res.end(
862
+ JSON.stringify({
863
+ jsonrpc: "2.0",
864
+ error: { code: -32e3, message: "Method not allowed." },
865
+ id: null
866
+ })
867
+ );
868
+ } else {
869
+ res.writeHead(405);
870
+ res.end("Method Not Allowed");
871
+ }
872
+ } else {
873
+ res.writeHead(404);
874
+ res.end("Not found");
875
+ }
876
+ } catch (err) {
877
+ console.error("HTTP server error:", err);
878
+ if (!res.headersSent) {
879
+ res.writeHead(500);
880
+ res.end("Internal server error");
881
+ }
882
+ }
883
+ }
884
+ );
885
+ const actualPort = await tryListen(httpServer, port, 10);
886
+ resolvedPort = actualPort;
887
+ mcpPort = actualPort;
888
+ console.log(`MCP \uC11C\uBC84 \uC2DC\uC791: http://localhost:${actualPort}/mcp`);
889
+ const cleanup = async () => {
890
+ if (globalBrowserTools) {
891
+ await globalBrowserTools.cleanup();
892
+ }
893
+ httpServer.close();
894
+ };
895
+ process.on("SIGINT", async () => {
896
+ await cleanup();
897
+ process.exit(0);
898
+ });
899
+ process.on("SIGTERM", async () => {
900
+ await cleanup();
901
+ process.exit(0);
902
+ });
903
+ return actualPort;
904
+ }
905
+ async function handleMCPRequest(id, payload) {
906
+ const url = `http://localhost:${mcpPort}/mcp`;
907
+ const res = await fetch(url, {
908
+ method: "POST",
909
+ headers: {
910
+ "Content-Type": "application/json",
911
+ Accept: "application/json, text/event-stream"
912
+ },
913
+ body: JSON.stringify(payload)
914
+ });
915
+ if (!res.ok) {
916
+ throw new Error(`MCP request failed: ${res.status} ${res.statusText}`);
917
+ }
918
+ const text = await res.text();
919
+ const lines = text.split("\n");
920
+ for (const line of lines) {
921
+ if (line.startsWith("data: ")) {
922
+ try {
923
+ return JSON.parse(line.slice(6));
924
+ } catch {
925
+ }
926
+ }
927
+ }
928
+ return null;
929
+ }
930
+
931
+ // src/cli/index.ts
932
+ import_commander.program.name("junis").description("AI\uAC00 \uB0B4 \uB514\uBC14\uC774\uC2A4\uB97C \uC644\uC804 \uC81C\uC5B4\uD558\uB294 MCP \uC11C\uBC84").version("0.1.0");
933
+ 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) => {
934
+ const port = parseInt(options.port, 10);
935
+ if (options.local) {
936
+ console.log("\u{1F3E0} \uB85C\uCEEC \uBAA8\uB4DC: \uD074\uB77C\uC6B0\uB4DC \uC5F0\uACB0 \uC5C6\uC774 MCP \uC11C\uBC84\uB9CC \uC2E4\uD589\uD569\uB2C8\uB2E4.");
937
+ const actualPort2 = await startMCPServer(port);
938
+ return;
939
+ }
940
+ let config = options.reset ? null : loadConfig();
941
+ if (!config) {
942
+ console.log("\u{1F511} Junis \uACC4\uC815 \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
943
+ const authResult = await authenticate();
944
+ config = {
945
+ device_key: authResult.device_key,
946
+ token: authResult.token,
947
+ device_name: `${process.env["USER"] ?? "user"}'s ${getDeviceName()}`,
948
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
949
+ };
950
+ saveConfig(config);
951
+ console.log(`\u{1F4BE} \uC778\uC99D \uC815\uBCF4 \uC800\uC7A5: ~/.junis/config.json`);
952
+ } else {
953
+ console.log(`\u{1F511} \uAE30\uC874 \uC778\uC99D \uC0AC\uC6A9: ${config.device_name}`);
954
+ }
955
+ const actualPort = await startMCPServer(port);
956
+ const relay = new RelayClient(config, handleMCPRequest);
957
+ await relay.connect();
958
+ console.log(`
959
+ \u2728 Junis \uC2DC\uC791 \uC644\uB8CC!
960
+ \uB85C\uCEEC MCP: http://localhost:${actualPort}/mcp
961
+ \uB514\uBC14\uC774\uC2A4: ${config.device_name}
962
+ \uC0C1\uD0DC: \uD074\uB77C\uC6B0\uB4DC \uB9B4\uB808\uC774 \uC5F0\uACB0\uB428
963
+ `);
964
+ });
965
+ import_commander.program.command("logout").description("\uC778\uC99D \uC815\uBCF4 \uC0AD\uC81C").action(() => {
966
+ clearConfig();
967
+ console.log("\u2705 \uC778\uC99D \uC815\uBCF4 \uC0AD\uC81C \uC644\uB8CC");
968
+ });
969
+ import_commander.program.command("status").description("\uD604\uC7AC \uC0C1\uD0DC \uD655\uC778").action(() => {
970
+ const config = loadConfig();
971
+ if (!config) {
972
+ console.log("\u274C \uC778\uC99D \uC5C6\uC74C (npx junis \uC2E4\uD589 \uD544\uC694)");
973
+ } else {
974
+ console.log(
975
+ `\u2705 \uC778\uC99D\uB428
976
+ \uB514\uBC14\uC774\uC2A4: ${config.device_name}
977
+ \uB4F1\uB85D\uC77C: ${config.created_at}`
978
+ );
979
+ }
980
+ });
981
+ function getDeviceName() {
982
+ const platform2 = process.platform;
983
+ if (platform2 === "darwin") return "Mac";
984
+ if (platform2 === "win32") return "Windows PC";
985
+ return "Linux PC";
986
+ }
987
+ import_commander.program.parse();