ispyx 1.0.0 → 1.0.2
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 +63 -122
- package/config.example.json +2 -1
- package/package.json +26 -6
- package/src/core/RESIDENT.md +1 -0
- package/src/core/guard.js +2 -0
- package/src/core/launcher.js +61 -3
- package/src/core/mode.js +1 -0
- package/src/core/panel/index.js +1 -1
- package/src/core/panel/page.html +12 -0
- package/src/core/resident.js +12 -1
- package/src/core/server.js +3 -2
- package/src/core/updates.js +83 -0
- package/src/macos-entry.js +8 -8
- package/src/server.js +2 -0
package/README.md
CHANGED
|
@@ -2,151 +2,107 @@
|
|
|
2
2
|
|
|
3
3
|
# ispyx
|
|
4
4
|
|
|
5
|
-
A local desktop-control [MCP](https://modelcontextprotocol.io) server. It lets an AI assistant use your computer the way a person does: look at the screen
|
|
5
|
+
A local desktop-control [MCP](https://modelcontextprotocol.io) server. It lets an AI assistant use your computer the way a person does: look at the screen, move the mouse, click, drag, scroll, type, press hotkeys and use the clipboard.
|
|
6
6
|
|
|
7
|
-
- Runs on your machine, talks to the
|
|
8
|
-
- Node 22
|
|
9
|
-
-
|
|
7
|
+
- Runs on your machine, talks to the AI app over **stdio**, opens **no network port** (the settings panel only listens while it is open).
|
|
8
|
+
- Windows and macOS. Windows needs Node.js 22+; the macOS app carries its own.
|
|
9
|
+
- One resident app (tray icon on Windows, menu-bar icon on macOS) does the work. Every AI app connects through a thin `ispyx mcp` process, so there is one stop key, one banner and one set of settings.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Install
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
- **Human-paced input.** The pointer glides along a gently curved path to each target, scrolling is smoothed, and typing goes one character at a time with natural pauses.
|
|
15
|
-
- **On-screen feedback.** An eye opens and a scan line sweeps the captured area after a screenshot; clicks, drags, scrolls, key presses, typing and waits each get a small animation; `list_windows` sends out a sonar ping that lights up window frames. A banner shows while a tool runs. All of it is click-through and can be turned off.
|
|
16
|
-
- **Safety.**
|
|
17
|
-
- **Alt+Esc** (Option+Esc on macOS) stops everything: one press cancels all running and queued calls, a second press within 5 s shuts the server down.
|
|
18
|
-
- A **denylist** of programs that are never driven (password managers by default).
|
|
19
|
-
- Refuses to type into **password fields**.
|
|
20
|
-
- An optional **password gate** that asks you before the first tool call.
|
|
21
|
-
- An action log of everything the server did.
|
|
22
|
-
- **Single resident app:** on Windows one background process (`ispyx --resident`, tray icon) does the work and every MCP client talks to it through a thin `ispyx mcp` process, so there is one stop key, one banner and one unlock state.
|
|
13
|
+
### Windows
|
|
23
14
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Open PowerShell (Start menu, type "PowerShell") and paste:
|
|
15
|
+
Open PowerShell and paste:
|
|
27
16
|
|
|
28
17
|
```
|
|
29
18
|
irm https://raw.githubusercontent.com/murat-mehmet/ispyx/master/scripts/install.ps1 | iex
|
|
30
19
|
```
|
|
31
20
|
|
|
32
|
-
No administrator rights
|
|
33
|
-
|
|
34
|
-
- finds Node.js 22+ or installs it for your user only (winget, or the official zip with a checksum check);
|
|
35
|
-
- installs ispyx with `npm install -g ispyx`;
|
|
36
|
-
- puts an `Ispyx` shortcut on your Desktop, turns on start at login and starts ispyx.
|
|
21
|
+
No administrator rights needed. It finds or installs Node.js 22+ for your user, runs `npm install -g ispyx`, adds an `Ispyx` shortcut to your Desktop, turns on start at login and starts ispyx. Manual: install Node.js from [nodejs.org](https://nodejs.org), then `npm i -g ispyx` and run `ispyx`.
|
|
37
22
|
|
|
38
|
-
|
|
23
|
+
Uninstall: run the installer with `-Uninstall` (add `-Purge` to also delete settings and logs). Options and troubleshooting: [docs/windows-install.md](docs/windows-install.md).
|
|
39
24
|
|
|
40
|
-
|
|
25
|
+
### macOS
|
|
41
26
|
|
|
42
|
-
|
|
27
|
+
Open Terminal and paste:
|
|
43
28
|
|
|
44
29
|
```
|
|
45
|
-
|
|
30
|
+
curl -fsSL https://github.com/murat-mehmet/ispyx/releases/latest/download/install.sh | sh
|
|
46
31
|
```
|
|
47
32
|
|
|
48
|
-
|
|
33
|
+
It downloads the newest `Ispyx.app`, checks it (SHA-256, the developer's signature, notarised) and puts it in `/Applications`, then opens it; allow the two permissions its window asks for. Or download the `.dmg` from the [Releases](https://github.com/murat-mehmet/ispyx/releases) page and drag Ispyx to Applications. The app carries its own Node, nothing else is needed. Uninstall: `... | sh -s -- --uninstall` (add `--purge` for settings and logs). Details: [native/macos/README.md](native/macos/README.md).
|
|
49
34
|
|
|
50
|
-
|
|
35
|
+
### Updating
|
|
51
36
|
|
|
52
|
-
|
|
37
|
+
The panel shows when a newer version is out, with an **Update** button. Or run `ispyx update` (macOS: `/Applications/Ispyx.app/Contents/Resources/ispyx update`, or the install command above again). Windows reinstalls the npm package; macOS replaces the app, which keeps its permissions. Settings and logs stay.
|
|
53
38
|
|
|
54
|
-
|
|
39
|
+
## Connect your AI app
|
|
55
40
|
|
|
56
|
-
|
|
41
|
+
Open the panel (tray or menu-bar icon, **Open**): it shows the exact command for your machine with a copy button. Or by hand:
|
|
57
42
|
|
|
58
43
|
```
|
|
59
|
-
claude mcp add ispyx --scope user --
|
|
44
|
+
claude mcp add ispyx --scope user -- ispyx mcp
|
|
60
45
|
```
|
|
61
46
|
|
|
62
|
-
Claude Desktop
|
|
47
|
+
Other apps (Claude Desktop, LM Studio, ...), JSON form. On Windows some apps need `"command": "cmd", "args": ["/c", "ispyx", "mcp"]`:
|
|
63
48
|
|
|
64
49
|
```json
|
|
65
50
|
{
|
|
66
51
|
"mcpServers": {
|
|
67
|
-
"ispyx": {
|
|
68
|
-
"command": "npx",
|
|
69
|
-
"args": ["-y", "ispyx"]
|
|
70
|
-
}
|
|
52
|
+
"ispyx": { "command": "ispyx", "args": ["mcp"] }
|
|
71
53
|
}
|
|
72
54
|
}
|
|
73
55
|
```
|
|
74
56
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
### Windows: the resident app
|
|
78
|
-
|
|
79
|
-
On Windows ispyx is a single resident app, like `Ispyx.app` on macOS:
|
|
80
|
-
|
|
81
|
-
- `ispyx` (in a console, or from a shortcut) starts the resident app hidden in the background if it is not running, opens its panel and exits.
|
|
82
|
-
- `ispyx --resident` is the resident itself (tray icon, effects, stop key). A second one exits at once. It writes no output; errors go to `%APPDATA%\ispyx\ispyx.log`.
|
|
83
|
-
- `ispyx mcp` is the stdio MCP server for your client. It forwards every call to the resident and starts it first if needed. Prefer registering `npx -y ispyx mcp`; an old registration without `mcp` still works (it is detected by the missing console).
|
|
84
|
-
- `ispyx quit` stops the resident (tray menu: Quit).
|
|
57
|
+
On macOS use `/Applications/Ispyx.app/Contents/Resources/ispyx` as the command, without `mcp`. The AI app starts ispyx when needed, so start at login is optional.
|
|
85
58
|
|
|
86
|
-
|
|
59
|
+
## Using it
|
|
87
60
|
|
|
88
|
-
macOS
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
Settings are applied in this order, later wins: built-in defaults, `config.json` (in the package folder, or the file named by `ISPYX_CONFIG`), environment variables. See `config.example.json`.
|
|
93
|
-
|
|
94
|
-
| `config.json` key | Environment variable | Default | Meaning |
|
|
95
|
-
| --- | --- | --- | --- |
|
|
96
|
-
| (the file itself) | `ISPYX_CONFIG` | `config.json` in the package root | Path of a config file. |
|
|
97
|
-
| `denylist` | `ISPYX_DENYLIST` (comma separated) | `keepass.exe`, `keepassxc.exe`, `1password.exe`, `bitwarden.exe`; macOS: `1Password`, `Bitwarden`, `KeePassXC`, `Keychain Access`, `Enpass` (app names, case-insensitive) | Programs that are never driven or focused. |
|
|
98
|
-
| `logPath` | `ISPYX_LOG` | `logs/actions.jsonl` (macOS: `~/Library/Application Support/Ispyx/logs/actions.jsonl`) | Action log (JSON lines). |
|
|
99
|
-
| `auth` | `ISPYX_AUTH` (`0`/`1`) | `false` | Ask for a password before the first tool call. |
|
|
100
|
-
| `authFile` | `ISPYX_AUTH_FILE` | `%APPDATA%\ispyx\auth.json` (macOS: `~/Library/Application Support/Ispyx/auth.json`) | Where the password hash is stored. |
|
|
101
|
-
| `overlay` | `ISPYX_OVERLAY` (`0`/`1`) | `true` | The "ispyx is working" banner. |
|
|
102
|
-
| `animations` | `ISPYX_ANIMATIONS` (`0`/`1`) | `true` | The eye, scan and other effects. |
|
|
103
|
-
| `typeSpeed` | `ISPYX_TYPE_SPEED` | `1` | Typing speed multiplier (2 = twice as fast, 0 = fixed 8 ms per character). |
|
|
104
|
-
| `stopChord` | `ISPYX_STOP_CHORD` | `alt+esc` | Emergency stop keys (mainly for tests). |
|
|
105
|
-
| `maxTypeLength` | | `10000` | Longest text one `type_text` call accepts. |
|
|
106
|
-
| `moveMinMs`, `moveMaxMs` | | `300`, `1000` | Duration range of pointer glides. |
|
|
107
|
-
| `typeMinMs`, `typeMaxMs`, `typeCapMs` | | `40`, `120`, `60000` | Pause range per typed character; a text is sped up to finish within the cap. |
|
|
108
|
-
|
|
109
|
-
The older `WIN_INPUT_*` names (and `WIN_INPUT_MCP_CONFIG`) still work as fallbacks; an `ISPYX_*` variable wins when both are set.
|
|
61
|
+
- **Panel.** Permissions (macOS), recent activity, effects, password, denylist, start at login, the connect command. Opens in its own small window and closes when you close it.
|
|
62
|
+
- **Commands.** `ispyx` starts the resident app and opens the panel, `ispyx mcp` is the stdio server for an AI app, `ispyx quit` stops it, `ispyx update` installs the latest version and restarts it. On macOS they are `/Applications/Ispyx.app/Contents/Resources/ispyx update` / `quit` (the app starts the resident itself). Errors go to `%APPDATA%\ispyx\ispyx.log` (macOS: `~/Library/Application Support/Ispyx/ispyx.log`). Details: [src/core/RESIDENT.md](src/core/RESIDENT.md).
|
|
63
|
+
- **Stop.** **Alt+Esc** (Option+Esc on macOS): one press cancels everything running, a second press within 5 s shuts the server down.
|
|
64
|
+
- **Feedback.** A banner shows while a tool runs (green working, amber cancelled, red stopped). An eye and a scan line show on screenshots, clicks, scrolls and typing get small animations. All of it is click-through and can be turned off in the panel.
|
|
110
65
|
|
|
111
66
|
## Tools
|
|
112
67
|
|
|
113
68
|
| Tool | What it does |
|
|
114
69
|
| --- | --- |
|
|
115
|
-
| `list_windows` |
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `move` | Move the pointer. |
|
|
123
|
-
| `mouse_down`, `mouse_up` | Press and release a mouse button. |
|
|
124
|
-
| `drag` | Press at one point, glide to another, release. |
|
|
125
|
-
| `scroll` | Smooth vertical and/or horizontal wheel, with optional modifiers. |
|
|
126
|
-
| `type_text` | Type Unicode text into the focused control at a human pace; refused in password fields. |
|
|
127
|
-
| `press_keys` | Key chords such as `ctrl+v`, `alt+tab`, `win+r`. |
|
|
128
|
-
| `key_down`, `key_up` | Hold and release keys. |
|
|
70
|
+
| `list_windows`, `list_monitors`, `cursor_position` | What is on the desktop and where the pointer is. |
|
|
71
|
+
| `focus_window` | Bring a window to the front. |
|
|
72
|
+
| `screenshot_screen`, `screenshot_window` | PNG of a monitor, all monitors, a region or one window. |
|
|
73
|
+
| `click`, `move`, `drag`, `scroll` | Pointer actions. Clicks take double/triple and held modifiers. |
|
|
74
|
+
| `mouse_down`, `mouse_up` | Press and release a button (the pointer moves straight while one is held). |
|
|
75
|
+
| `type_text`, `press_keys`, `key_down`, `key_up` | Typing, hotkeys such as `ctrl+v`, hold and release. |
|
|
76
|
+
| `get_clipboard`, `set_clipboard` | Read text; write text, an image or a file. |
|
|
129
77
|
| `wait` | Pause up to 60 s. |
|
|
130
|
-
| `get_clipboard`, `set_clipboard` | Read clipboard text; write text, an image or a file. |
|
|
131
78
|
|
|
132
|
-
Typical flow: `screenshot_screen`, read coordinates off the image, `click` (with `space: "shot"`) or `type_text`, then `screenshot_screen` to check.
|
|
79
|
+
Typical flow: `screenshot_screen`, read coordinates off the image, `click` (with `space: "shot"`) or `type_text`, then `screenshot_screen` to check. Coordinates and per-tool behaviour: [docs/behavior.md](docs/behavior.md). Input is human-paced: the pointer glides along a curved path, scrolling is smoothed, typing has natural pauses.
|
|
133
80
|
|
|
134
|
-
## Safety
|
|
81
|
+
## Safety
|
|
135
82
|
|
|
136
|
-
- **Alt+Esc**
|
|
137
|
-
- **
|
|
138
|
-
- **
|
|
139
|
-
- **
|
|
140
|
-
- **Action log.** Every action, refusal and error is logged locally, including typed text (
|
|
141
|
-
-
|
|
83
|
+
- **Alt+Esc** is caught by a low-level keyboard hook, so it does not also cycle windows.
|
|
84
|
+
- **Denylist** of programs that are never focused or driven (password managers by default); editable in the panel.
|
|
85
|
+
- **No credentials.** Typing into password fields is refused; ispyx never reads or types saved passwords.
|
|
86
|
+
- **Password gate** (optional, set it in the panel): asks before the first tool call.
|
|
87
|
+
- **Action log.** Every action, refusal and error is logged locally, including typed text (not text aimed at a password field). Clipboard reads log the length only.
|
|
88
|
+
- **Elevated windows** cannot be controlled on Windows (UIPI), and neither can UAC prompts or the lock screen.
|
|
89
|
+
- This gives an AI control of your computer. Connect it only to apps you trust.
|
|
142
90
|
|
|
143
|
-
##
|
|
91
|
+
## Configuration
|
|
144
92
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
|
148
|
-
|
|
|
149
|
-
|
|
|
93
|
+
Most settings are in the panel. Files and variables, later wins: built-in defaults, `config.json` (package folder or the file in `ISPYX_CONFIG`), the per-user `%APPDATA%\ispyx\config.json` (macOS: `~/Library/Application Support/Ispyx/`), environment variables. See `config.example.json`.
|
|
94
|
+
|
|
95
|
+
| Key | Variable | Default |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| `denylist` | `ISPYX_DENYLIST` (comma separated) | common password managers |
|
|
98
|
+
| `auth` | `ISPYX_AUTH` (`0`/`1`) | `false` |
|
|
99
|
+
| `overlay`, `animations` | `ISPYX_OVERLAY`, `ISPYX_ANIMATIONS` (`0`/`1`) | `true` |
|
|
100
|
+
| `typeSpeed` | `ISPYX_TYPE_SPEED` | `1` (2 = twice as fast) |
|
|
101
|
+
| `stopChord` | `ISPYX_STOP_CHORD` | `alt+esc` |
|
|
102
|
+
| `logPath` | `ISPYX_LOG` | `logs/actions.jsonl` |
|
|
103
|
+
| `authFile` | `ISPYX_AUTH_FILE` | `%APPDATA%\ispyx\auth.json` |
|
|
104
|
+
|
|
105
|
+
More keys (`maxTypeLength`, `moveMinMs`/`moveMaxMs`, `typeMinMs`/`typeMaxMs`/`typeCapMs`) are in `config.example.json`. The older `WIN_INPUT_*` names still work as fallbacks.
|
|
150
106
|
|
|
151
107
|
## Development
|
|
152
108
|
|
|
@@ -154,31 +110,16 @@ Typical flow: `screenshot_screen`, read coordinates off the image, `click` (with
|
|
|
154
110
|
git clone https://github.com/murat-mehmet/ispyx
|
|
155
111
|
cd ispyx
|
|
156
112
|
npm install
|
|
157
|
-
|
|
113
|
+
npm test # unit checks; other scripts in docs/testing.md
|
|
158
114
|
```
|
|
159
115
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
- `src/
|
|
163
|
-
-
|
|
164
|
-
- `src/effects/` the on-screen animations.
|
|
165
|
-
- `native/macos/` the macOS app (Swift).
|
|
166
|
-
- `src/server.js` is the entry point (`bin`) and only loads `src/core/server.js`.
|
|
167
|
-
- `test/` tests; most drive the real desktop of the machine they run on.
|
|
168
|
-
|
|
169
|
-
Releases (npm package and macOS DMG) are made by pushing a version tag, see [docs/releasing.md](docs/releasing.md).
|
|
170
|
-
|
|
171
|
-
```
|
|
172
|
-
npm test # unit checks (no desktop needed for most)
|
|
173
|
-
npm run smoke # drives the real server against Notepad
|
|
174
|
-
npm run motion-test # pointer glide duration, curvature, exact arrival
|
|
175
|
-
npm run typing-test # typing pace and accuracy
|
|
176
|
-
npm run instances-test
|
|
177
|
-
npm run shutdown-test
|
|
178
|
-
```
|
|
116
|
+
- `src/core/` shared: MCP server and tools, safety guard, input motion, resident app, panel, password gate.
|
|
117
|
+
- `src/platform/` the OS layer (contract in `src/platform/CONTRACT.md`): `win32/` (Win32 through `koffi`), `darwin/` (a client of `Ispyx.app`).
|
|
118
|
+
- `src/effects/` animations. `native/macos/` the macOS app (Swift). `scripts/install.ps1` the Windows installer.
|
|
119
|
+
- Releases (npm package and macOS DMG) are made by pushing a version tag: [docs/releasing.md](docs/releasing.md).
|
|
179
120
|
|
|
180
|
-
|
|
121
|
+
Tests drive the real desktop and use their own settings (own stop chord, own pipe), so they never touch a server you are using.
|
|
181
122
|
|
|
182
123
|
## License
|
|
183
124
|
|
|
184
|
-
|
|
125
|
+
See [LICENSE](LICENSE).
|
package/config.example.json
CHANGED
package/package.json
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ispyx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Local desktop-control MCP server: lets an AI use your keyboard, mouse and screen (windows, screenshots, clicks, typing, clipboard). stdio only, Windows and macOS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server.js",
|
|
7
|
-
"bin": {
|
|
7
|
+
"bin": {
|
|
8
|
+
"ispyx": "src/server.js"
|
|
9
|
+
},
|
|
8
10
|
"files": [
|
|
9
11
|
"src/",
|
|
10
12
|
"README.md",
|
|
11
13
|
"LICENSE",
|
|
12
14
|
"config.example.json"
|
|
13
15
|
],
|
|
14
|
-
"engines": {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"computer-use",
|
|
23
|
+
"desktop-automation",
|
|
24
|
+
"windows",
|
|
25
|
+
"macos",
|
|
26
|
+
"screenshot",
|
|
27
|
+
"keyboard",
|
|
28
|
+
"mouse",
|
|
29
|
+
"claude"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/murat-mehmet/ispyx.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/murat-mehmet/ispyx/issues"
|
|
37
|
+
},
|
|
18
38
|
"homepage": "https://github.com/murat-mehmet/ispyx#readme",
|
|
19
39
|
"scripts": {
|
|
20
40
|
"start": "node src/server.js",
|
package/src/core/RESIDENT.md
CHANGED
|
@@ -12,6 +12,7 @@ process is started with an explicit `--resident` / `mcp` argument (used by tests
|
|
|
12
12
|
| `ispyx --resident` | The single owner of the pipe (`ipc.listenOwner`). Runs the guard, stop hotkey, effects, tool queue, tray and panel. No stdio. A second resident finds the pipe taken and exits at once (code 0). |
|
|
13
13
|
| `ispyx mcp` | Thin stdio MCP server: same tool list and schemas, every call forwarded with `callOwner`. If no resident runs it starts one first (up to 10 s). If the resident dies mid-call the call returns a tool error. Keeps a watch connection so the resident's shutdown (second stop press, tray Quit) also ends it. |
|
|
14
14
|
| `ispyx quit` | Ask the resident to exit. |
|
|
15
|
+
| `ispyx update` | Stop the resident, `npm install -g ispyx@latest`, start the resident again if it was running (Windows). |
|
|
15
16
|
| `ispyx` without a console on stdin | Old MCP registration pointing at `src/server.js`: behaves as `mcp`. |
|
|
16
17
|
|
|
17
18
|
Mode selection is `src/core/mode.js`. Start/reach helpers are `src/core/launcher.js`; resident extras are
|
package/src/core/guard.js
CHANGED
|
@@ -29,6 +29,7 @@ const DEFAULTS = {
|
|
|
29
29
|
maxTypeLength: 10000,
|
|
30
30
|
auth: false, // ask for a password (src/auth.js) before the first tool call; env WIN_INPUT_AUTH
|
|
31
31
|
authFile: '', // where the password hash and unlock time live; default %APPDATA%\ispyx\auth.json
|
|
32
|
+
updateCheck: true, // the resident asks GitHub once a day for a newer release (shown in the panel); env ISPYX_UPDATE_CHECK
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
/**
|
|
@@ -79,6 +80,7 @@ export function loadConfig(source = process.env) {
|
|
|
79
80
|
if (e('TYPE_SPEED')) cfg.typeSpeed = Number(e('TYPE_SPEED'));
|
|
80
81
|
if (e('OVERLAY')) cfg.overlay = !/^(0|false|no)$/i.test(e('OVERLAY'));
|
|
81
82
|
if (e('ANIMATIONS')) cfg.animations = !/^(0|false|no)$/i.test(e('ANIMATIONS'));
|
|
83
|
+
if (e('UPDATE_CHECK')) cfg.updateCheck = !/^(0|false|no)$/i.test(e('UPDATE_CHECK'));
|
|
82
84
|
cfg.denylist = cfg.denylist.map((s) => String(s).trim().toLowerCase()).filter(Boolean);
|
|
83
85
|
if (e('LOG')) cfg.logPath = e('LOG');
|
|
84
86
|
cfg.logPath = path.resolve(ROOT, cfg.logPath);
|
package/src/core/launcher.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// Starting and reaching the resident app (resident model). No platform import: usable before anything is loaded.
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
3
6
|
import { fileURLToPath } from 'node:url';
|
|
4
7
|
import { callOwner, OwnerGone, pipeName } from './ipc.js';
|
|
5
8
|
|
|
@@ -13,12 +16,30 @@ function macApp() {
|
|
|
13
16
|
return m ? m[1] : null;
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
const MAC_BUNDLE_ID = 'com.ispyx.app';
|
|
20
|
+
/** Where Ispyx.app's resident listens (macos-entry.js pins it there for the app's own processes). */
|
|
21
|
+
export const macAppPipe = () => path.join(os.homedir(), 'Library', 'Application Support', 'Ispyx', 'ispyx.sock');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* ispyx from npm/npx on a Mac with Ispyx.app installed: forward to the app's resident instead of running a second
|
|
25
|
+
* server of its own (one stop key, one banner, the calls in the panel). Not for tests (ISPYX_PLATFORM) or an explicit
|
|
26
|
+
* ISPYX_PIPE. Returns true when this process should be the `mcp` forwarder.
|
|
27
|
+
*/
|
|
28
|
+
export function useMacAppResident(env = process.env) {
|
|
29
|
+
if (process.platform !== 'darwin' || macApp() || env.ISPYX_PLATFORM || env.ISPYX_PIPE) return false;
|
|
30
|
+
const installed = ['/Applications', path.join(os.homedir(), 'Applications')].some((d) => fs.existsSync(path.join(d, 'Ispyx.app')));
|
|
31
|
+
if (!installed) return false;
|
|
32
|
+
env.ISPYX_PIPE = macAppPipe();
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
16
36
|
/** Starts `ispyx --resident` detached and hidden, with this process's environment (same pipe). Inside Ispyx.app it
|
|
17
37
|
* opens the app instead, which starts the resident itself. */
|
|
18
38
|
export function spawnResident() {
|
|
19
39
|
const app = macApp();
|
|
20
|
-
const
|
|
21
|
-
|
|
40
|
+
const forApp = app || (process.platform === 'darwin' && process.env.ISPYX_PIPE === macAppPipe());
|
|
41
|
+
const child = forApp
|
|
42
|
+
? spawn('open', ['-g', ...(app ? [app] : ['-b', MAC_BUNDLE_ID]), '--args', '--background'], { stdio: 'ignore', detached: true })
|
|
22
43
|
: spawn(process.execPath, [ENTRY, '--resident'], { detached: true, windowsHide: true, stdio: 'ignore' });
|
|
23
44
|
child.on('error', () => {});
|
|
24
45
|
child.unref();
|
|
@@ -63,8 +84,45 @@ export async function runLauncher() {
|
|
|
63
84
|
process.exit(0);
|
|
64
85
|
}
|
|
65
86
|
|
|
87
|
+
/**
|
|
88
|
+
* `ispyx update`. Windows: stops the resident (it holds files open), installs the latest package, starts it again if
|
|
89
|
+
* it ran. macOS: the app is updated as a whole (it is signed), by scripts/install.sh from the latest release.
|
|
90
|
+
*/
|
|
91
|
+
export async function runUpdate() {
|
|
92
|
+
if (process.platform === 'darwin') {
|
|
93
|
+
const { MAC_INSTALL_URL } = await import('./updates.js');
|
|
94
|
+
const code = await new Promise((resolve) => {
|
|
95
|
+
const c = spawn('/bin/sh', ['-c', `curl -fsSL ${MAC_INSTALL_URL} | sh`], { stdio: 'inherit' });
|
|
96
|
+
c.on('error', () => resolve(1));
|
|
97
|
+
c.on('exit', (n) => resolve(n ?? 1));
|
|
98
|
+
});
|
|
99
|
+
process.exit(code);
|
|
100
|
+
}
|
|
101
|
+
const pipe = pipeName();
|
|
102
|
+
const wasRunning = await residentRunning(pipe);
|
|
103
|
+
if (wasRunning) {
|
|
104
|
+
try { await callOwner(pipe, '__quit', {}); } catch { /* gone before answering */ }
|
|
105
|
+
for (let i = 0; i < 40 && await residentRunning(pipe); i++) await sleep(150);
|
|
106
|
+
}
|
|
107
|
+
const code = await new Promise((resolve) => {
|
|
108
|
+
const c = spawn('npm', ['install', '-g', 'ispyx@latest'], { stdio: 'inherit', shell: true });
|
|
109
|
+
c.on('error', () => resolve(1));
|
|
110
|
+
c.on('exit', (n) => resolve(n ?? 1));
|
|
111
|
+
});
|
|
112
|
+
if (code !== 0) process.stderr.write('ispyx: the update failed (see the npm output above).\n');
|
|
113
|
+
// Start the new copy: npm replaced the files in place, so a fresh process loads them.
|
|
114
|
+
if (wasRunning) spawnResident();
|
|
115
|
+
process.exit(code);
|
|
116
|
+
}
|
|
117
|
+
|
|
66
118
|
/** `ispyx quit`: asks the resident to exit; exits 0 either way. */
|
|
67
119
|
export async function runQuit() {
|
|
120
|
+
// macOS: the app would start its resident again; quitting the app ends it and the forwarders
|
|
121
|
+
useMacAppResident();
|
|
122
|
+
if (process.platform === 'darwin' && (macApp() || process.env.ISPYX_PIPE === macAppPipe())) {
|
|
123
|
+
spawnSync('osascript', ['-e', `quit app id "${MAC_BUNDLE_ID}"`], { stdio: 'ignore', timeout: 10000 });
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
68
126
|
try { await callOwner(pipeName(), '__quit', {}); } catch { /* not running, or gone before answering */ }
|
|
69
127
|
process.exit(0);
|
|
70
128
|
}
|
package/src/core/mode.js
CHANGED
|
@@ -11,6 +11,7 @@ export function resolveMode(argv = process.argv.slice(2), env = process.env, pla
|
|
|
11
11
|
if (argv.includes('--resident')) return 'resident';
|
|
12
12
|
if (argv.includes('mcp')) return 'mcp';
|
|
13
13
|
if (argv.includes('quit') || argv.includes('--quit')) return 'quit';
|
|
14
|
+
if (argv.includes('update')) return 'update';
|
|
14
15
|
const target = env.ISPYX_PLATFORM || platform;
|
|
15
16
|
if (target === 'win32') return isTTY ? 'launcher' : 'mcp';
|
|
16
17
|
return 'classic';
|
package/src/core/panel/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
const PAGE = fileURLToPath(new URL('./page.html', import.meta.url));
|
|
9
9
|
const COOKIE = 'ispyx_panel';
|
|
10
10
|
// Explicit whitelist; an action is also callable only if the host's `actions` object defines it.
|
|
11
|
-
const ACTIONS = ['quit', 'setAutostart', 'setEffects', 'openLog', 'copy', 'close', 'requestPermission', 'openPermissionSettings', 'showConfigFile', 'clearRecent', 'addDeny', 'removeDeny', 'setPassword', 'setAuthRequired'];
|
|
11
|
+
const ACTIONS = ['quit', 'setAutostart', 'setEffects', 'openLog', 'copy', 'close', 'requestPermission', 'openPermissionSettings', 'showConfigFile', 'clearRecent', 'addDeny', 'removeDeny', 'setPassword', 'setAuthRequired', 'update', 'checkUpdate'];
|
|
12
12
|
const BOOL_ACTIONS = new Set(['setAutostart', 'setEffects']);
|
|
13
13
|
const STRING_ACTIONS = new Set(['copy', 'requestPermission', 'openPermissionSettings', 'addDeny']); // value must be a short string
|
|
14
14
|
// value is an object of short strings (and a boolean for setAuthRequired.on)
|
package/src/core/panel/page.html
CHANGED
|
@@ -17,6 +17,8 @@ header{display:flex;align-items:center;gap:14px;margin-bottom:18px}
|
|
|
17
17
|
header img{width:44px;height:44px;flex:none;border-radius:10px}
|
|
18
18
|
h1{font-size:17px;line-height:1.25;margin:0;font-weight:700}
|
|
19
19
|
.sub{color:var(--mute);font-size:12px;margin-top:2px;overflow-wrap:anywhere}
|
|
20
|
+
.upd{display:flex;align-items:center;gap:8px;margin-top:6px;color:var(--fg)}
|
|
21
|
+
.upd button{padding:2px 10px;font-size:12px}
|
|
20
22
|
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--mute);margin-right:6px;vertical-align:1px}
|
|
21
23
|
.run .dot{background:var(--acc);box-shadow:0 0 0 3px rgba(47,209,107,.22)}
|
|
22
24
|
.warn .dot{background:var(--warn)}
|
|
@@ -93,6 +95,7 @@ button.link:hover{background:none;color:var(--fg)}
|
|
|
93
95
|
<h1 id="head">Ispyx</h1>
|
|
94
96
|
<div class="sub" id="state"><span class="dot"></span><span id="stateTxt">Connecting</span></div>
|
|
95
97
|
<div class="sub" id="meta"></div>
|
|
98
|
+
<div class="sub upd" id="upd" hidden><span id="updTxt"></span> <button type="button" class="pri" id="updBtn">Update</button></div>
|
|
96
99
|
</div>
|
|
97
100
|
</header>
|
|
98
101
|
<div id="off" hidden>Ispyx has quit. You can close this window.</div>
|
|
@@ -269,6 +272,9 @@ function render(s){
|
|
|
269
272
|
if(typeof s.uptimeMs==="number")meta.push("up "+fmt(s.uptimeMs));
|
|
270
273
|
if(s.pid!=null)meta.push("pid "+s.pid);
|
|
271
274
|
setText("meta",meta.join(" · "));
|
|
275
|
+
var u=s.update;
|
|
276
|
+
show("upd",!!(u&&u.available));
|
|
277
|
+
if(u&&u.available&&!updating)setText("updTxt","Version "+u.latest+" is available.");
|
|
272
278
|
|
|
273
279
|
show("sPerm",!!(perms&&perms.length));
|
|
274
280
|
if(perms&&perms.length)renderPerms(perms);
|
|
@@ -421,6 +427,12 @@ $("cfgShow").addEventListener("click",function(){act("showConfigFile")});
|
|
|
421
427
|
$("log").addEventListener("click",function(){act("openLog")});
|
|
422
428
|
$("showAll").addEventListener("click",function(){expanded=!expanded;if(last&&last.recent)renderRecent(last.recent,last.recentTotal)});
|
|
423
429
|
$("clearAct").addEventListener("click",function(){act("clearRecent").then(poll)});
|
|
430
|
+
// the update quits and restarts ispyx: this window then reports it has quit (macOS opens it again)
|
|
431
|
+
var updating=false;
|
|
432
|
+
$("updBtn").addEventListener("click",function(){
|
|
433
|
+
updating=true;$("updBtn").disabled=true;setText("updTxt","Updating, ispyx restarts in a moment…");
|
|
434
|
+
act("update").catch(function(err){updating=false;$("updBtn").disabled=false;setText("updTxt",err.message)});
|
|
435
|
+
});
|
|
424
436
|
$("end").addEventListener("click",function(){
|
|
425
437
|
if(last&&last.platform==="darwin"){act("close");return}
|
|
426
438
|
if(!confirm("Quit Ispyx? AI tools lose access until it is reopened."))return;
|
package/src/core/resident.js
CHANGED
|
@@ -22,6 +22,7 @@ import { spawn } from 'node:child_process';
|
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
23
|
|
|
24
24
|
import { saveUserConfig } from './guard.js';
|
|
25
|
+
import { startUpdateCheck, launchUpdate } from './updates.js';
|
|
25
26
|
import { PasswordError } from './auth.js';
|
|
26
27
|
|
|
27
28
|
/** A refusal whose message goes to the person at the panel. */
|
|
@@ -119,6 +120,8 @@ const safe = (fn, dflt) => { try { return fn(); } catch { return dflt; } };
|
|
|
119
120
|
export function panelModel(ctx) {
|
|
120
121
|
const { w, config, guard, auth, scan, clients } = ctx;
|
|
121
122
|
const t0 = Date.now();
|
|
123
|
+
// never from tests (fake platform): they must not depend on the network
|
|
124
|
+
const updates = startUpdateCheck({ current: VERSION, enabled: config.updateCheck !== false && w.name !== 'fake' });
|
|
122
125
|
const save = (patch) => {
|
|
123
126
|
try { saveUserConfig(patch); } catch (e) { throw new PanelError(`Could not save the setting: ${e.message}`); }
|
|
124
127
|
};
|
|
@@ -135,6 +138,7 @@ export function panelModel(ctx) {
|
|
|
135
138
|
passwordSet: safe(() => auth.passwordSet(), false),
|
|
136
139
|
authRequired: !!config.auth,
|
|
137
140
|
denylist: config.denylist.slice(),
|
|
141
|
+
...(updates.state() ? { update: updates.state() } : {}),
|
|
138
142
|
});
|
|
139
143
|
const actions = {
|
|
140
144
|
setEffects: (on) => { scan.enabled = !!on; config.animations = !!on; save({ animations: !!on }); return !!on; },
|
|
@@ -163,8 +167,15 @@ export function panelModel(ctx) {
|
|
|
163
167
|
save({ denylist: config.denylist.slice() });
|
|
164
168
|
},
|
|
165
169
|
clearRecent: () => { recent.length = 0; },
|
|
170
|
+
// the newer release: installs it in a process of its own, which quits and restarts ispyx (this one included)
|
|
171
|
+
update: () => {
|
|
172
|
+
if (!updates.state()?.available) throw new PanelError('This is the newest version.');
|
|
173
|
+
launchUpdate({ platform: w.name, dataDir: w.dataDir() });
|
|
174
|
+
return true;
|
|
175
|
+
},
|
|
176
|
+
checkUpdate: async () => { await updates.check(); return updates.state(); },
|
|
166
177
|
};
|
|
167
|
-
return { getState, actions };
|
|
178
|
+
return { getState, actions, stop: () => updates.stop() };
|
|
168
179
|
}
|
|
169
180
|
|
|
170
181
|
/**
|
package/src/core/server.js
CHANGED
|
@@ -13,13 +13,14 @@ import { Auth } from './auth.js';
|
|
|
13
13
|
import { ScanAnimation, keyLabels } from '../effects/scan.js';
|
|
14
14
|
import { pipeName, listenOwner, callOwner, watchOwner, OwnerGone } from './ipc.js';
|
|
15
15
|
import { resolveMode } from './mode.js';
|
|
16
|
-
import { ensureResident } from './launcher.js';
|
|
16
|
+
import { ensureResident, useMacAppResident } from './launcher.js';
|
|
17
17
|
import { panelModel, recordCall, setupLogging, startResident } from './resident.js';
|
|
18
18
|
|
|
19
19
|
const { parseSequence, insertsContent, Overlay, installStopHotkey } = w;
|
|
20
20
|
|
|
21
21
|
// Windows: one resident app does the work and `ispyx mcp` forwards to it (see RESIDENT.md). Elsewhere: classic election.
|
|
22
|
-
|
|
22
|
+
// npm/npx on a Mac with Ispyx.app installed: forward to the app's resident rather than run a second server
|
|
23
|
+
const mode = resolveMode() === 'classic' && useMacAppResident() ? 'mcp' : resolveMode();
|
|
23
24
|
const config = loadConfig();
|
|
24
25
|
// Only the process that owns the pipe (see ipc.js) does the work; the others forward their tool calls to it.
|
|
25
26
|
let guard, overlay, auth, stopHook = null;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Newer-release check for the panel and `ispyx update`. Asks GitHub which release is the latest (an outgoing HTTPS
|
|
2
|
+
// request, no API key, no data sent), at most once a day; config `updateCheck: false` turns it off.
|
|
3
|
+
// Updating stays per platform:
|
|
4
|
+
// Windows `ispyx update`: stop the resident, npm install -g ispyx@latest, start it again (launcher.js)
|
|
5
|
+
// macOS scripts/install.sh from the latest release: checks and swaps Ispyx.app, then opens it again
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
export const REPO = 'murat-mehmet/ispyx';
|
|
12
|
+
export const MAC_INSTALL_URL = `https://github.com/${REPO}/releases/latest/download/install.sh`;
|
|
13
|
+
const ENTRY = fileURLToPath(new URL('../server.js', import.meta.url));
|
|
14
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
15
|
+
|
|
16
|
+
/** 1 when a is newer than b, -1 when older, 0 when equal; "1.2.10" > "1.2.9", a release beats its pre-release. */
|
|
17
|
+
export function compareVersions(a, b) {
|
|
18
|
+
const parse = (v) => {
|
|
19
|
+
const [main, pre = ''] = String(v).replace(/^v/, '').split('-', 2);
|
|
20
|
+
return { nums: main.split('.').map((n) => Number(n) || 0), pre };
|
|
21
|
+
};
|
|
22
|
+
const x = parse(a), y = parse(b);
|
|
23
|
+
for (let i = 0; i < Math.max(x.nums.length, y.nums.length); i++) {
|
|
24
|
+
const d = (x.nums[i] ?? 0) - (y.nums[i] ?? 0);
|
|
25
|
+
if (d) return Math.sign(d);
|
|
26
|
+
}
|
|
27
|
+
if (x.pre === y.pre) return 0;
|
|
28
|
+
if (!x.pre) return 1;
|
|
29
|
+
if (!y.pre) return -1;
|
|
30
|
+
return x.pre > y.pre ? 1 : -1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The version of the latest release, from the redirect of /releases/latest (no API call, so no rate limit). */
|
|
34
|
+
export async function latestVersion({ fetchImpl = fetch, timeoutMs = 10000 } = {}) {
|
|
35
|
+
const res = await fetchImpl(`https://github.com/${REPO}/releases/latest`, {
|
|
36
|
+
method: 'HEAD', redirect: 'manual', signal: AbortSignal.timeout(timeoutMs),
|
|
37
|
+
});
|
|
38
|
+
const m = /\/releases\/tag\/v?([^/?#]+)$/.exec(res.headers.get('location') ?? '');
|
|
39
|
+
if (!m) throw new Error(`no latest release found (HTTP ${res.status})`);
|
|
40
|
+
return decodeURIComponent(m[1]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Checks shortly after start and then once a day. state() is { current, latest, available } once a check has
|
|
45
|
+
* answered, else null; a failed check (offline) keeps the last answer and is retried on the next round.
|
|
46
|
+
*/
|
|
47
|
+
export function startUpdateCheck({ current, enabled = true, firstDelayMs = 15000, fetchImpl } = {}) {
|
|
48
|
+
let latest = null;
|
|
49
|
+
const check = async () => {
|
|
50
|
+
try { latest = await latestVersion({ fetchImpl }); } catch { /* offline or GitHub down: try again later */ }
|
|
51
|
+
};
|
|
52
|
+
const timers = [];
|
|
53
|
+
if (enabled) {
|
|
54
|
+
const t = setTimeout(check, firstDelayMs);
|
|
55
|
+
const i = setInterval(check, DAY_MS);
|
|
56
|
+
t.unref?.();
|
|
57
|
+
i.unref?.();
|
|
58
|
+
timers.push(t, i);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
check,
|
|
62
|
+
state: () => (latest ? { current, latest, available: compareVersions(latest, current) > 0 } : null),
|
|
63
|
+
stop: () => { for (const t of timers) clearTimeout(t); },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Starts the update in a process of its own (the update quits the running ispyx, this process included) and
|
|
69
|
+
* returns at once. Its output goes to update.log in the data directory.
|
|
70
|
+
*/
|
|
71
|
+
export function launchUpdate({ platform = process.platform, dataDir }) {
|
|
72
|
+
let out = 'ignore';
|
|
73
|
+
try {
|
|
74
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
75
|
+
out = fs.openSync(path.join(dataDir, 'update.log'), 'w');
|
|
76
|
+
} catch { /* no log then */ }
|
|
77
|
+
const child = platform === 'darwin'
|
|
78
|
+
? spawn('/bin/sh', ['-c', `curl -fsSL ${MAC_INSTALL_URL} | sh`], { detached: true, stdio: ['ignore', out, out] })
|
|
79
|
+
: spawn(process.execPath, [ENTRY, 'update'], { detached: true, windowsHide: true, stdio: ['ignore', out, out] });
|
|
80
|
+
child.on('error', () => {});
|
|
81
|
+
child.unref();
|
|
82
|
+
if (typeof out === 'number') fs.closeSync(out);
|
|
83
|
+
}
|
package/src/macos-entry.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// Entry point used by Ispyx.app: the app starts it as the resident (`--resident`), and the MCP launcher
|
|
2
|
-
// (Contents/Resources/ispyx) as the thin forwarder (`mcp`)
|
|
3
|
-
// environment of the MCP client says, then runs the normal
|
|
2
|
+
// (Contents/Resources/ispyx) as the thin forwarder (`mcp`), or for `update` / `quit`. Picks the platform
|
|
3
|
+
// automatically (darwin), whatever the environment of the MCP client says, then runs the normal entry point.
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
-
import os from 'node:os';
|
|
6
5
|
import path from 'node:path';
|
|
6
|
+
import { macAppPipe } from './core/launcher.js';
|
|
7
7
|
|
|
8
8
|
delete process.env.ISPYX_PLATFORM;
|
|
9
9
|
// The app's resident and every forwarder meet on one socket in the app's data folder, whatever their environment
|
|
10
|
-
// (an MCP client may give its children another temp directory).
|
|
10
|
+
// (an MCP client may give its children another temp directory). npm copies on this Mac use it too (launcher.js).
|
|
11
11
|
if (!process.env.ISPYX_PIPE) {
|
|
12
|
-
const
|
|
13
|
-
try { fs.mkdirSync(
|
|
14
|
-
process.env.ISPYX_PIPE =
|
|
12
|
+
const sock = macAppPipe();
|
|
13
|
+
try { fs.mkdirSync(path.dirname(sock), { recursive: true, mode: 0o700 }); } catch { /* reported when listening fails */ }
|
|
14
|
+
process.env.ISPYX_PIPE = sock;
|
|
15
15
|
}
|
|
16
|
-
await import('./
|
|
16
|
+
await import('./server.js'); // the entry point: picks resident / mcp / update / quit
|
package/src/server.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// ispyx --resident the resident app (tray, panel, tool queue)
|
|
5
5
|
// ispyx mcp stdio MCP server forwarding to the resident (started on demand)
|
|
6
6
|
// ispyx quit ask the resident to exit
|
|
7
|
+
// ispyx update stop the resident, npm install -g ispyx@latest, start it again
|
|
7
8
|
// Windows without a console on stdin (an old MCP registration) behaves as `mcp`. Other platforms run the
|
|
8
9
|
// classic server (core/server.js), unchanged. See src/core/RESIDENT.md.
|
|
9
10
|
import { resolveMode } from "./core/mode.js";
|
|
@@ -11,4 +12,5 @@ import { resolveMode } from "./core/mode.js";
|
|
|
11
12
|
const mode = resolveMode();
|
|
12
13
|
if (mode === "launcher") await (await import("./core/launcher.js")).runLauncher();
|
|
13
14
|
else if (mode === "quit") await (await import("./core/launcher.js")).runQuit();
|
|
15
|
+
else if (mode === "update") await (await import("./core/launcher.js")).runUpdate();
|
|
14
16
|
else await import("./core/server.js");
|