junis 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,160 +3,196 @@
3
3
  [![npm version](https://img.shields.io/npm/v/junis)](https://www.npmjs.com/package/junis)
4
4
  [![license](https://img.shields.io/npm/l/junis)](./LICENSE)
5
5
 
6
- Full device control for AI agents via MCP. Run a local MCP server or connect to the cloud so any AI agent can execute commands, manage files, control browsers, and more on your machine.
6
+ AI 에이전트가 컴퓨터를 완전 제어할 있게 해주는 로컬 MCP 도구 서버입니다.
7
+ 셸 명령 실행, 파일 관리, 브라우저 자동화, 노트북 편집, 화면/카메라 캡처 등 28개 도구를 제공합니다.
7
8
 
8
9
  ---
9
10
 
10
- ## Features
11
+ ## 특징
11
12
 
12
- - **Zero-config local mode** — `npx junis --local` and you're live
13
- - **Cloud relay mode**connect via `wss://api.junis.ai` for remote access
14
- - **File & shell tools**read, write, search, execute, manage processes
15
- - **Browser automation**navigate, click, type, screenshot, PDF via Playwright
16
- - **MCP-native**works with Claude Code, Claude Desktop, and any MCP client
17
- - **TypeScript**built on `@modelcontextprotocol/sdk`
13
+ - **제로 설정 로컬 모드** — `npx junis --local` 줄이면 바로 사용 가능
14
+ - **클라우드 릴레이 모드** — `wss://api.junis.ai`를 통해 원격 디바이스 제어
15
+ - **파일 & 도구**읽기, 쓰기, 검색, 명령 실행, 프로세스 관리
16
+ - **브라우저 자동화**Playwright 기반 탐색, 클릭, 입력, 스크린샷, PDF 저장
17
+ - **노트북 도구** Jupyter 노트북 읽기, 수정, 추가, 삭제, 실행
18
+ - **디바이스 도구** 화면 캡처, 카메라, 알림, 클립보드, 녹화, 위치, 오디오
19
+ - **MCP 호환** — Claude Code, Claude Desktop 등 모든 MCP 클라이언트와 연동
20
+ - **TypeScript** — `@modelcontextprotocol/sdk` 기반
18
21
 
19
22
  ---
20
23
 
21
- ## Quick Start
24
+ ## 설치 및 실행
22
25
 
23
- ### Local Mode (no account required)
26
+ ### 로컬 모드 (계정 불필요)
24
27
 
25
28
  ```bash
26
29
  npx junis --local
27
30
  ```
28
31
 
29
- Starts an MCP server on `http://localhost:3000`. Point your MCP client at it and start using tools immediately.
32
+ `http://localhost:3000/mcp`에 MCP 서버가 시작됩니다. MCP 클라이언트에서 주소를 지정하면 바로 사용할 있습니다.
30
33
 
31
- ### Cloud Mode
34
+ ### 클라우드 모드
32
35
 
33
36
  ```bash
34
37
  npx junis
35
38
  ```
36
39
 
37
- On first run, opens a browser for OAuth login. After authentication, your device connects to `wss://api.junis.ai` and is ready for remote AI agent sessions.
40
+ 최초 실행 브라우저가 열려 OAuth 로그인을 진행합니다. 인증 디바이스가 `wss://api.junis.ai`에 연결되어 원격 AI 에이전트 세션을 받을 있습니다. 백그라운드 데몬으로 실행되며 부팅 시 자동 시작됩니다.
41
+
42
+ ### 포트 변경
43
+
44
+ ```bash
45
+ npx junis --local --port 8080
46
+ ```
47
+
48
+ ---
49
+
50
+ ## 제공 도구 목록
51
+
52
+ ### FilesystemTools (8개)
53
+
54
+ | 도구 | 설명 |
55
+ |------|------|
56
+ | `execute_command` | 터미널 명령 실행 (타임아웃, 백그라운드 실행 지원) |
57
+ | `read_file` | 파일 읽기 (utf-8, base64 인코딩 지원) |
58
+ | `write_file` | 파일 쓰기/생성 (상위 디렉토리 자동 생성) |
59
+ | `edit_block` | 파일의 특정 텍스트 블록을 교체 (diff 기반 부분 수정) |
60
+ | `list_directory` | 디렉토리 내 파일/폴더 목록 조회 |
61
+ | `search_code` | 코드/텍스트 패턴 검색 (ripgrep 우선, fallback으로 glob 검색) |
62
+ | `list_processes` | 실행 중인 프로세스 목록 (CPU 사용률 순) |
63
+ | `kill_process` | 프로세스 종료 (SIGTERM 후 3초 대기, 미종료 시 SIGKILL 자동 적용) |
64
+
65
+ ### BrowserTools (7개)
66
+
67
+ > Playwright 설치 필요: `npx playwright install chromium`
68
+ > 미설치 시 브라우저 도구 호출 시 경고 메시지만 출력되며 서버가 중단되지 않습니다.
69
+
70
+ | 도구 | 설명 |
71
+ |------|------|
72
+ | `browser_navigate` | URL로 이동 |
73
+ | `browser_click` | CSS 선택자로 요소 클릭 |
74
+ | `browser_type` | 텍스트 입력 (기존 내용 삭제 후 입력 옵션) |
75
+ | `browser_screenshot` | 현재 페이지 스크린샷 (파일 저장 또는 base64 반환) |
76
+ | `browser_snapshot` | 페이지 접근성 트리(aria snapshot) 조회 |
77
+ | `browser_evaluate` | 페이지 내 JavaScript 실행 |
78
+ | `browser_pdf` | 현재 페이지를 PDF로 저장 |
79
+
80
+ ### NotebookTools (5개)
81
+
82
+ > Jupyter 설치 필요: `pip install jupyter`
83
+
84
+ | 도구 | 설명 |
85
+ |------|------|
86
+ | `notebook_read` | .ipynb 노트북 셀 목록 및 내용 조회 |
87
+ | `notebook_edit_cell` | 특정 셀 소스 코드 수정 |
88
+ | `notebook_add_cell` | 새 셀 추가 (code/markdown, 위치 지정 가능) |
89
+ | `notebook_delete_cell` | 특정 셀 삭제 |
90
+ | `notebook_execute` | 노트북 전체 실행 (nbconvert --execute) |
91
+
92
+ ### DeviceTools (8개)
93
+
94
+ | 도구 | 설명 |
95
+ |------|------|
96
+ | `screen_capture` | 화면 스크린샷 (macOS: screencapture, Linux: scrot) |
97
+ | `camera_capture` | 카메라 사진 촬영 (macOS: imagesnap, Linux: fswebcam) |
98
+ | `notification_send` | OS 알림 전송 |
99
+ | `clipboard_read` | 클립보드 내용 읽기 |
100
+ | `clipboard_write` | 클립보드에 텍스트 쓰기 |
101
+ | `screen_record` | 화면 녹화 시작/중지 (macOS: screencapture -v, 기타: ffmpeg) |
102
+ | `location_get` | 현재 위치 조회 (macOS: CoreLocation, fallback: IP 기반) |
103
+ | `audio_play` | 오디오 파일 재생 (macOS: afplay, 기타: ffplay) |
38
104
 
39
105
  ---
40
106
 
41
- ## CLI Reference
107
+ ## CLI 명령어
42
108
 
43
109
  ```
44
110
  Usage: npx junis [options] [command]
111
+ ```
45
112
 
46
- Options:
47
- --local Run MCP server locally without cloud authentication
48
- --port <port> Port for local MCP server (default: 3000)
49
- --reset Reset saved credentials and re-authenticate
113
+ | 명령어 | 설명 |
114
+ |--------|------|
115
+ | `start` (기본) | Junis 에이전트 시작. 인증 후 백그라운드 데몬으로 실행 |
116
+ | `stop` | 백그라운드 서비스 중지 및 자동시작 해제 |
117
+ | `logout` | 저장된 인증 정보 삭제 |
118
+ | `status` | 현재 연결 및 디바이스 상태 확인 |
50
119
 
51
- Commands:
52
- logout Remove stored credentials
53
- status Show current connection and device status
54
- ```
120
+ ### 옵션
121
+
122
+ | 옵션 | 설명 |
123
+ |------|------|
124
+ | `--local` | 클라우드 연결 없이 로컬 MCP 서버만 실행 |
125
+ | `--port <number>` | 로컬 MCP 서버 포트 (기본: 3000) |
126
+ | `--reset` | 기존 인증 초기화 후 재로그인 |
55
127
 
56
- ### Examples
128
+ ### 사용 예시
57
129
 
58
130
  ```bash
59
- # Start local MCP server on default port
131
+ # 로컬 MCP 서버 실행 (기본 포트)
60
132
  npx junis --local
61
133
 
62
- # Start local MCP server on custom port
134
+ # 로컬 MCP 서버 실행 (포트 지정)
63
135
  npx junis --local --port 8080
64
136
 
65
- # Re-authenticate (clear saved token)
137
+ # 클라우드 모드로 시작 (최초 실행 시 인증)
138
+ npx junis
139
+
140
+ # 인증 초기화 후 재로그인
66
141
  npx junis --reset
67
142
 
68
- # Check connection status
143
+ # 백그라운드 서비스 중지
144
+ npx junis stop
145
+
146
+ # 연결 상태 확인
69
147
  npx junis status
70
148
 
71
- # Log out
149
+ # 로그아웃
72
150
  npx junis logout
73
151
  ```
74
152
 
75
153
  ---
76
154
 
77
- ## Tools
155
+ ## Claude Code 연동
78
156
 
79
- ### File & Shell
157
+ junis를 stdio MCP 서버로 등록하면 Claude Code가 자동으로 실행합니다:
80
158
 
81
- | Tool | Description |
82
- |------|-------------|
83
- | `execute_command` | Run shell commands and return stdout/stderr |
84
- | `read_file` | Read file contents |
85
- | `write_file` | Write or overwrite a file |
86
- | `list_directory` | List files and directories |
87
- | `search_code` | Search for text patterns across files |
88
- | `list_processes` | List running processes |
89
- | `kill_process` | Kill a process by PID |
90
-
91
- ### Browser (requires Playwright)
92
-
93
- | Tool | Description |
94
- |------|-------------|
95
- | `browser_navigate` | Navigate to a URL |
96
- | `browser_click` | Click an element by selector |
97
- | `browser_type` | Type text into an input |
98
- | `browser_screenshot` | Take a screenshot of the page |
99
- | `browser_snapshot` | Capture DOM snapshot for accessibility |
100
- | `browser_evaluate` | Execute JavaScript in the page |
101
- | `browser_pdf` | Save the page as a PDF |
102
-
103
- > Playwright is optional. If not installed, browser tools emit a warning instead of crashing.
104
-
105
- ### Notebook (requires Jupyter)
106
-
107
- | Tool | Description |
108
- |------|-------------|
109
- | `notebook_read` | Read Jupyter notebook cells |
110
- | `notebook_edit_cell` | Edit a specific cell in a notebook |
111
- | `notebook_execute` | Execute notebook via nbconvert |
112
-
113
- > Install Jupyter: `pip install jupyter`
114
-
115
- ### Device (macOS / Linux / Windows)
116
-
117
- | Tool | Description |
118
- |------|-------------|
119
- | `screen_capture` | Capture a screenshot using OS native tools |
120
- | `camera_capture` | Take a photo using the webcam |
121
- | `notification_send` | Send an OS notification |
122
- | `clipboard_read` | Read clipboard contents |
123
- | `clipboard_write` | Write text to clipboard |
159
+ ```bash
160
+ claude mcp add junis -- npx junis --local
161
+ ```
124
162
 
125
- > macOS: `brew install imagesnap` for camera capture
163
+ 등록 Claude Code에서 별도 서버 시작 없이 모든 도구를 바로 사용할 수 있습니다.
126
164
 
127
165
  ---
128
166
 
129
- ## Claude Code Integration
130
-
131
- Register junis as a stdio MCP server so Claude Code can launch it automatically:
132
-
133
- ```bash
134
- claude mcp add junis node /Users/leehyeonjin/development/junis-mcp/dist/server/stdio.js
135
- ```
167
+ ## 알려진 제한사항
136
168
 
137
- After registration, Claude Code will start junis automatically and have access to all tools without any manual server startup.
169
+ - **camera_capture (macOS 데몬 모드)**: macOS에서 데몬(백그라운드) 모드로 실행 카메라에 접근하려면 **시스템 설정 > 개인정보 보호 보안 > 카메라**에서 `node`에 권한을 직접 부여해야 합니다.
170
+ - **screen_capture (macOS)**: 화면 녹화 권한이 필요합니다. **시스템 설정 > 개인정보 보호 및 보안 > 화면 및 시스템 오디오 녹음**에서 터미널 앱에 권한을 부여하세요.
171
+ - **browser 도구**: Playwright가 설치되어 있지 않으면 브라우저 도구 호출 시 경고만 출력됩니다. `npx playwright install chromium`으로 설치하세요.
172
+ - **notebook 도구**: Jupyter가 설치되어 있지 않으면 노트북 실행(`notebook_execute`)이 실패합니다. `pip install jupyter`로 설치하세요.
173
+ - **location_get**: macOS에서 CoreLocationCLI가 없으면 IP 기반 위치 추정으로 자동 전환됩니다 (정확도 낮음).
138
174
 
139
175
  ---
140
176
 
141
- ## Environment Variables
177
+ ## 환경 변수
142
178
 
143
- | Variable | Default | Description |
144
- |----------|---------|-------------|
145
- | `JUNIS_API_URL` | `https://api.junis.ai` | REST API base URL |
146
- | `JUNIS_WS_URL` | `wss://api.junis.ai` | WebSocket relay URL |
147
- | `JUNIS_WEB_URL` | `https://junis.ai` | Web UI URL (used during OAuth) |
179
+ | 변수 | 기본값 | 설명 |
180
+ |------|--------|------|
181
+ | `JUNIS_API_URL` | `https://api.junis.ai` | REST API 기본 URL |
182
+ | `JUNIS_WS_URL` | `wss://api.junis.ai` | WebSocket 릴레이 URL |
183
+ | `JUNIS_WEB_URL` | `https://junis.ai` | UI URL (OAuth 사용) |
148
184
 
149
185
  ---
150
186
 
151
- ## Requirements
187
+ ## 요구 사항
152
188
 
153
189
  - Node.js >= 18
154
- - (Optional) Playwright for browser tools: `npx playwright install chromium`
155
- - (Optional) Jupyter for notebook tools: `pip install jupyter`
156
- - (Optional) imagesnap for camera capture (macOS): `brew install imagesnap`
190
+ - (선택) Playwright 브라우저 도구: `npx playwright install chromium`
191
+ - (선택) Jupyter 노트북 도구: `pip install jupyter`
192
+ - (선택) imagesnap macOS 카메라 캡처: `brew install imagesnap`
157
193
 
158
194
  ---
159
195
 
160
- ## License
196
+ ## 라이선스
161
197
 
162
198
  MIT
package/dist/cli/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "junis",
34
- version: "0.2.4",
34
+ version: "0.2.6",
35
35
  description: "One-line device control for AI agents",
36
36
  bin: {
37
37
  junis: "dist/cli/index.js"
@@ -43,9 +43,11 @@ var require_package = __commonJS({
43
43
  prepublishOnly: "npm run build"
44
44
  },
45
45
  dependencies: {
46
+ "@inquirer/prompts": "^8.2.1",
46
47
  "@modelcontextprotocol/sdk": "^1.0.0",
47
48
  commander: "^12.0.0",
48
49
  glob: "^11.0.0",
50
+ "node-notifier": "^10.0.1",
49
51
  open: "^10.1.0",
50
52
  playwright: "^1.49.0",
51
53
  ws: "^8.18.0",
@@ -53,6 +55,7 @@ var require_package = __commonJS({
53
55
  },
54
56
  devDependencies: {
55
57
  "@types/node": "^20.0.0",
58
+ "@types/node-notifier": "^8.0.5",
56
59
  "@types/ws": "^8.5.0",
57
60
  tsup: "^8.0.0",
58
61
  tsx: "^4.0.0",
@@ -70,6 +73,7 @@ var require_package = __commonJS({
70
73
 
71
74
  // src/cli/index.ts
72
75
  var import_commander = require("commander");
76
+ var import_prompts = require("@inquirer/prompts");
73
77
 
74
78
  // src/cli/config.ts
75
79
  var import_fs = __toESM(require("fs"));
@@ -836,6 +840,7 @@ var NotebookTools = class {
836
840
  var import_child_process3 = require("child_process");
837
841
  var import_util3 = require("util");
838
842
  var import_zod4 = require("zod");
843
+ var import_node_notifier = __toESM(require("node-notifier"));
839
844
  var execAsync3 = (0, import_util3.promisify)(import_child_process3.exec);
840
845
  var screenRecordPid = null;
841
846
  function platform() {
@@ -924,20 +929,23 @@ var DeviceTools = class {
924
929
  message: import_zod4.z.string().describe("\uC54C\uB9BC \uB0B4\uC6A9")
925
930
  },
926
931
  async ({ title, message }) => {
927
- const p = platform();
928
- let cmd;
929
- if (p === "win") {
930
- const script = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${message.replace(/'/g, "''")}', '${title.replace(/'/g, "''")}')`;
931
- const encoded = Buffer.from(script, "utf16le").toString("base64");
932
- cmd = `powershell -NoProfile -EncodedCommand ${encoded}`;
933
- } else {
934
- cmd = {
935
- mac: `osascript -e 'display notification "${message.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"'`,
936
- linux: `notify-send "${title.replace(/"/g, '\\"')}" "${message.replace(/"/g, '\\"')}"`
937
- }[p] ?? "";
932
+ try {
933
+ await new Promise((resolve, reject) => {
934
+ import_node_notifier.default.notify(
935
+ { title, message },
936
+ (err) => {
937
+ if (err) reject(err);
938
+ else resolve();
939
+ }
940
+ );
941
+ });
942
+ return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
943
+ } catch (err) {
944
+ return {
945
+ content: [{ type: "text", text: `\uC54C\uB9BC \uC804\uC1A1 \uC2E4\uD328: ${err.message}` }],
946
+ isError: true
947
+ };
938
948
  }
939
- await execAsync3(cmd);
940
- return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
941
949
  }
942
950
  );
943
951
  server.tool(
@@ -1522,8 +1530,154 @@ function printStep1(port) {
1522
1530
  console.log(` \u25C9 Local MCP endpoint ........... http://localhost:${port}/mcp`);
1523
1531
  console.log("");
1524
1532
  }
1525
- import_commander.program.command("start", { isDefault: true }).description("Junis \uC5D0\uC774\uC804\uD2B8\uC640 \uC5F0\uACB0 \uC2DC\uC791").option("--local", "\uB85C\uCEEC MCP \uC11C\uBC84\uB9CC \uC2E4\uD589 (\uD074\uB77C\uC6B0\uB4DC \uC5F0\uACB0 \uC5C6\uC74C)").option("--port <number>", "\uD3EC\uD2B8 \uBC88\uD638", "3000").option("--reset", "\uAE30\uC874 \uC778\uC99D \uCD08\uAE30\uD654 \uD6C4 \uC7AC\uB85C\uADF8\uC778").option("--daemon", "\uB370\uBAAC \uBAA8\uB4DC\uB85C \uC2E4\uD589 (\uB0B4\uBD80\uC6A9, launchd/systemd\uC5D0\uC11C \uC0AC\uC6A9)").action(async (options) => {
1533
+ async function runForeground(config, port) {
1534
+ const deviceName = config.device_name;
1535
+ const platformName = process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux";
1536
+ const actualPort = await startMCPServer(port);
1537
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1538
+ console.log(" STEP 5 \xB7 Starting Foreground Service");
1539
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1540
+ console.log(` \u25C9 MCP server started on port ${actualPort}`);
1541
+ const relay = new RelayClient(config, handleMCPRequest, async () => {
1542
+ console.log("[junis] \uC138\uC158 \uB9CC\uB8CC - \uC7AC\uC778\uC99D \uD544\uC694");
1543
+ try {
1544
+ let waitingPrinted = false;
1545
+ const authResult = await authenticate(
1546
+ deviceName,
1547
+ platformName,
1548
+ (uri) => {
1549
+ console.log(`[junis] \uBE0C\uB77C\uC6B0\uC800 \uC7AC\uC778\uC99D: ${uri}`);
1550
+ },
1551
+ () => {
1552
+ if (!waitingPrinted) waitingPrinted = true;
1553
+ }
1554
+ );
1555
+ config.token = authResult.token;
1556
+ saveConfig(config);
1557
+ relay.restart();
1558
+ } catch (e) {
1559
+ console.error("[junis] \uC7AC\uC778\uC99D \uC2E4\uD328:", e);
1560
+ process.exit(1);
1561
+ }
1562
+ });
1563
+ await relay.connect();
1564
+ const webUrl = process.env.JUNIS_WEB_URL ?? "https://junis.ai";
1565
+ console.log(" \u25C9 Relay connected");
1566
+ console.log("");
1567
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1568
+ console.log(" \u2705 ALL SET \u2014 Junis\uAC00 \uD3EC\uADF8\uB77C\uC6B4\uB4DC\uC5D0\uC11C \uC2E4\uD589 \uC911\uC785\uB2C8\uB2E4.");
1569
+ console.log(" Ctrl+C\uB97C \uB20C\uB7EC \uC885\uB8CC\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
1570
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1571
+ console.log("");
1572
+ console.log(` \u2192 ${webUrl}`);
1573
+ console.log("");
1574
+ const shutdown = () => {
1575
+ console.log("\n[junis] Shutting down...");
1576
+ relay.destroy();
1577
+ process.exit(0);
1578
+ };
1579
+ process.on("SIGINT", shutdown);
1580
+ process.on("SIGTERM", shutdown);
1581
+ }
1582
+ async function runBackground(config, port) {
1583
+ void config;
1584
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1585
+ console.log(" STEP 5 \xB7 Starting Background Service");
1586
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1587
+ const svc = new ServiceManager();
1588
+ try {
1589
+ await svc.install();
1590
+ console.log(" \u25C9 Service registered ........... \u2705");
1591
+ console.log(" \u25C9 Auto-start on boot ........... \u2705");
1592
+ } catch (e) {
1593
+ console.warn(` \u26A0\uFE0F \uC11C\uBE44\uC2A4 \uB4F1\uB85D \uC2E4\uD328: ${e.message}`);
1594
+ console.warn(" \uBC31\uADF8\uB77C\uC6B4\uB4DC \uD504\uB85C\uC138\uC2A4\uB85C\uB9CC \uC2E4\uD589\uD569\uB2C8\uB2E4.");
1595
+ startDaemon(port);
1596
+ }
1597
+ const webUrl = process.env.JUNIS_WEB_URL ?? "https://junis.ai";
1598
+ console.log("");
1599
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1600
+ console.log(" \u2705 ALL SET \u2014 Junis\uAC00 \uBC31\uADF8\uB77C\uC6B4\uB4DC\uC5D0\uC11C \uC2E4\uD589 \uC911\uC785\uB2C8\uB2E4.");
1601
+ console.log(" \uBD80\uD305 \uC2DC \uC790\uB3D9\uC73C\uB85C \uC2DC\uC791\uB429\uB2C8\uB2E4.");
1602
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1603
+ console.log("");
1604
+ console.log(` \u2192 ${webUrl}`);
1605
+ console.log("");
1606
+ console.log(" \uC885\uB8CC\uD558\uB824\uBA74: npx junis stop");
1607
+ console.log("");
1608
+ process.exit(0);
1609
+ }
1610
+ import_commander.program.command("start", { isDefault: true }).description("Junis \uC5D0\uC774\uC804\uD2B8\uC640 \uC5F0\uACB0 \uC2DC\uC791").option("--local", "\uB85C\uCEEC MCP \uC11C\uBC84\uB9CC \uC2E4\uD589 (\uD074\uB77C\uC6B0\uB4DC \uC5F0\uACB0 \uC5C6\uC74C)").option("--port <number>", "\uD3EC\uD2B8 \uBC88\uD638", "3000").option("--reset", "\uAE30\uC874 \uC778\uC99D \uCD08\uAE30\uD654 \uD6C4 \uC7AC\uB85C\uADF8\uC778").option("--daemon", "\uB370\uBAAC \uBAA8\uB4DC\uB85C \uC2E4\uD589 (\uB0B4\uBD80\uC6A9, launchd/systemd\uC5D0\uC11C \uC0AC\uC6A9)").option("--foreground", "\uD3EC\uADF8\uB77C\uC6B4\uB4DC \uBAA8\uB4DC\uB85C \uC2E4\uD589 (\uD504\uB86C\uD504\uD2B8 \uC5C6\uC774)").action(async (options) => {
1526
1611
  const port = parseInt(options.port, 10);
1612
+ if (options.foreground) {
1613
+ printBanner();
1614
+ let config2 = options.reset ? null : loadConfig();
1615
+ const deviceName2 = config2?.device_name ?? `${process.env["USER"] ?? "user"}'s ${getDeviceName()}`;
1616
+ const platformName2 = process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux";
1617
+ printStep1(port);
1618
+ if (!config2) {
1619
+ let waitingPrinted = false;
1620
+ const authResult = await authenticate(
1621
+ deviceName2,
1622
+ platformName2,
1623
+ (uri) => {
1624
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1625
+ console.log(" STEP 2 \xB7 Connect to Junis Cloud");
1626
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1627
+ console.log(" Opening browser...");
1628
+ console.log(` \u2192 ${uri}`);
1629
+ process.stdout.write(" Waiting for login \xB7");
1630
+ },
1631
+ () => {
1632
+ if (!waitingPrinted) {
1633
+ waitingPrinted = true;
1634
+ } else {
1635
+ process.stdout.write("\xB7");
1636
+ }
1637
+ }
1638
+ );
1639
+ console.log("");
1640
+ console.log(` \u2705 Authenticated as ${authResult.email ?? "your account"}`);
1641
+ console.log("");
1642
+ config2 = {
1643
+ device_key: authResult.device_key,
1644
+ token: authResult.token,
1645
+ device_name: deviceName2,
1646
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
1647
+ };
1648
+ saveConfig(config2);
1649
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1650
+ console.log(" STEP 3 \xB7 Register Device");
1651
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1652
+ console.log(` \u25C9 Device name .................. ${deviceName2}`);
1653
+ console.log(` \u25C9 Device key ................... ${authResult.device_key}`);
1654
+ console.log(` \u25C9 Cloud relay connected ........ wss://junis.ai/ws/devices/${authResult.device_key}`);
1655
+ console.log(" \u25C9 Status ....................... \u{1F7E2} online");
1656
+ console.log("");
1657
+ if (authResult.agent_id) {
1658
+ const workspaceName = `${deviceName2} Workspace`;
1659
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1660
+ console.log(" STEP 4 \xB7 Create AI Team");
1661
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1662
+ console.log(` \u25C9 Workspace created ............ ${workspaceName}`);
1663
+ console.log(` \u25C9 Agent created ................ ${authResult.agent_name}`);
1664
+ console.log(` \u25C9 Device linked ................ ${deviceName2} \u2192 ${authResult.agent_name}`);
1665
+ console.log(" \u25C9 Tools assigned ............... call_device_mcp, list_device_mcp_tools");
1666
+ console.log("");
1667
+ }
1668
+ } else {
1669
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1670
+ console.log(" STEP 3 \xB7 Register Device");
1671
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1672
+ console.log(` \u25C9 Device name .................. ${config2.device_name}`);
1673
+ console.log(` \u25C9 Device key ................... ${config2.device_key}`);
1674
+ console.log(` \u25C9 Cloud relay connected ........ wss://junis.ai/ws/devices/${config2.device_key}`);
1675
+ console.log(" \u25C9 Status ....................... \u{1F7E2} online");
1676
+ console.log("");
1677
+ }
1678
+ await runForeground(config2, port);
1679
+ return;
1680
+ }
1527
1681
  if (options.daemon) {
1528
1682
  writePid(process.pid);
1529
1683
  if (options.local) {
@@ -1565,8 +1719,8 @@ import_commander.program.command("start", { isDefault: true }).description("Juni
1565
1719
  console.log("[junis daemon] relay connected");
1566
1720
  return;
1567
1721
  }
1568
- printBanner();
1569
1722
  if (options.local) {
1723
+ printBanner();
1570
1724
  const actualPort = await startMCPServer(port);
1571
1725
  printStep1(actualPort);
1572
1726
  console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
@@ -1574,6 +1728,7 @@ import_commander.program.command("start", { isDefault: true }).description("Juni
1574
1728
  console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1575
1729
  return;
1576
1730
  }
1731
+ printBanner();
1577
1732
  const { running, pid } = isRunning();
1578
1733
  if (running) {
1579
1734
  console.log(`\u2705 Junis \uC2E4\uD589 \uC911\uC785\uB2C8\uB2E4. (PID: ${pid})`);
@@ -1644,31 +1799,26 @@ import_commander.program.command("start", { isDefault: true }).description("Juni
1644
1799
  console.log(" \u25C9 Status ....................... \u{1F7E2} online");
1645
1800
  console.log("");
1646
1801
  }
1647
- console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1648
- console.log(" STEP 5 \xB7 Starting Background Service");
1649
- console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1650
- const svc = new ServiceManager();
1651
- try {
1652
- await svc.install();
1653
- console.log(" \u25C9 Service registered ........... \u2705");
1654
- console.log(" \u25C9 Auto-start on boot ........... \u2705");
1655
- } catch (e) {
1656
- console.warn(` \u26A0\uFE0F \uC11C\uBE44\uC2A4 \uB4F1\uB85D \uC2E4\uD328: ${e.message}`);
1657
- console.warn(" \uBC31\uADF8\uB77C\uC6B4\uB4DC \uD504\uB85C\uC138\uC2A4\uB85C\uB9CC \uC2E4\uD589\uD569\uB2C8\uB2E4.");
1658
- startDaemon(port);
1802
+ const mode = await (0, import_prompts.select)({
1803
+ message: "Select run mode:",
1804
+ choices: [
1805
+ {
1806
+ name: "Foreground",
1807
+ value: "foreground",
1808
+ description: "Runs in the current terminal. Press Ctrl+C to stop.\n Full OS access: camera, notifications, and more."
1809
+ },
1810
+ {
1811
+ name: "Background (daemon)",
1812
+ value: "background",
1813
+ description: "Runs as a background service. Stays alive after\n closing the terminal. Auto-starts on reboot."
1814
+ }
1815
+ ]
1816
+ });
1817
+ if (mode === "foreground") {
1818
+ await runForeground(config, port);
1819
+ } else {
1820
+ await runBackground(config, port);
1659
1821
  }
1660
- const webUrl = process.env.JUNIS_WEB_URL ?? "https://junis.ai";
1661
- console.log("");
1662
- console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1663
- console.log(" \u2705 ALL SET \u2014 Junis\uAC00 \uBC31\uADF8\uB77C\uC6B4\uB4DC\uC5D0\uC11C \uC2E4\uD589 \uC911\uC785\uB2C8\uB2E4.");
1664
- console.log(" \uBD80\uD305 \uC2DC \uC790\uB3D9\uC73C\uB85C \uC2DC\uC791\uB429\uB2C8\uB2E4.");
1665
- console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1666
- console.log("");
1667
- console.log(` \u2192 ${webUrl}`);
1668
- console.log("");
1669
- console.log(" \uC885\uB8CC\uD558\uB824\uBA74: npx junis stop");
1670
- console.log("");
1671
- process.exit(0);
1672
1822
  });
1673
1823
  import_commander.program.command("stop").description("\uBC31\uADF8\uB77C\uC6B4\uB4DC \uC11C\uBE44\uC2A4 \uC911\uC9C0 \uBC0F \uC790\uB3D9\uC2DC\uC791 \uD574\uC81C").action(async () => {
1674
1824
  const stopped = stopDaemon();
@@ -573,6 +573,7 @@ var NotebookTools = class {
573
573
  var import_child_process3 = require("child_process");
574
574
  var import_util3 = require("util");
575
575
  var import_zod4 = require("zod");
576
+ var import_node_notifier = __toESM(require("node-notifier"));
576
577
  var execAsync3 = (0, import_util3.promisify)(import_child_process3.exec);
577
578
  var screenRecordPid = null;
578
579
  function platform() {
@@ -661,20 +662,23 @@ var DeviceTools = class {
661
662
  message: import_zod4.z.string().describe("\uC54C\uB9BC \uB0B4\uC6A9")
662
663
  },
663
664
  async ({ title, message }) => {
664
- const p = platform();
665
- let cmd;
666
- if (p === "win") {
667
- const script = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${message.replace(/'/g, "''")}', '${title.replace(/'/g, "''")}')`;
668
- const encoded = Buffer.from(script, "utf16le").toString("base64");
669
- cmd = `powershell -NoProfile -EncodedCommand ${encoded}`;
670
- } else {
671
- cmd = {
672
- mac: `osascript -e 'display notification "${message.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"'`,
673
- linux: `notify-send "${title.replace(/"/g, '\\"')}" "${message.replace(/"/g, '\\"')}"`
674
- }[p] ?? "";
665
+ try {
666
+ await new Promise((resolve, reject) => {
667
+ import_node_notifier.default.notify(
668
+ { title, message },
669
+ (err) => {
670
+ if (err) reject(err);
671
+ else resolve();
672
+ }
673
+ );
674
+ });
675
+ return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
676
+ } catch (err) {
677
+ return {
678
+ content: [{ type: "text", text: `\uC54C\uB9BC \uC804\uC1A1 \uC2E4\uD328: ${err.message}` }],
679
+ isError: true
680
+ };
675
681
  }
676
- await execAsync3(cmd);
677
- return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
678
682
  }
679
683
  );
680
684
  server.tool(
@@ -562,6 +562,7 @@ var NotebookTools = class {
562
562
  var import_child_process3 = require("child_process");
563
563
  var import_util3 = require("util");
564
564
  var import_zod4 = require("zod");
565
+ var import_node_notifier = __toESM(require("node-notifier"));
565
566
  var execAsync3 = (0, import_util3.promisify)(import_child_process3.exec);
566
567
  var screenRecordPid = null;
567
568
  function platform() {
@@ -650,20 +651,23 @@ var DeviceTools = class {
650
651
  message: import_zod4.z.string().describe("\uC54C\uB9BC \uB0B4\uC6A9")
651
652
  },
652
653
  async ({ title, message }) => {
653
- const p = platform();
654
- let cmd;
655
- if (p === "win") {
656
- const script = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${message.replace(/'/g, "''")}', '${title.replace(/'/g, "''")}')`;
657
- const encoded = Buffer.from(script, "utf16le").toString("base64");
658
- cmd = `powershell -NoProfile -EncodedCommand ${encoded}`;
659
- } else {
660
- cmd = {
661
- mac: `osascript -e 'display notification "${message.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"'`,
662
- linux: `notify-send "${title.replace(/"/g, '\\"')}" "${message.replace(/"/g, '\\"')}"`
663
- }[p] ?? "";
654
+ try {
655
+ await new Promise((resolve, reject) => {
656
+ import_node_notifier.default.notify(
657
+ { title, message },
658
+ (err) => {
659
+ if (err) reject(err);
660
+ else resolve();
661
+ }
662
+ );
663
+ });
664
+ return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
665
+ } catch (err) {
666
+ return {
667
+ content: [{ type: "text", text: `\uC54C\uB9BC \uC804\uC1A1 \uC2E4\uD328: ${err.message}` }],
668
+ isError: true
669
+ };
664
670
  }
665
- await execAsync3(cmd);
666
- return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
667
671
  }
668
672
  );
669
673
  server.tool(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "junis",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "One-line device control for AI agents",
5
5
  "bin": {
6
6
  "junis": "dist/cli/index.js"
@@ -12,9 +12,11 @@
12
12
  "prepublishOnly": "npm run build"
13
13
  },
14
14
  "dependencies": {
15
+ "@inquirer/prompts": "^8.2.1",
15
16
  "@modelcontextprotocol/sdk": "^1.0.0",
16
17
  "commander": "^12.0.0",
17
18
  "glob": "^11.0.0",
19
+ "node-notifier": "^10.0.1",
18
20
  "open": "^10.1.0",
19
21
  "playwright": "^1.49.0",
20
22
  "ws": "^8.18.0",
@@ -22,6 +24,7 @@
22
24
  },
23
25
  "devDependencies": {
24
26
  "@types/node": "^20.0.0",
27
+ "@types/node-notifier": "^8.0.5",
25
28
  "@types/ws": "^8.5.0",
26
29
  "tsup": "^8.0.0",
27
30
  "tsx": "^4.0.0",