@laivikto/dsh-ssh-servers 0.0.0-stage → 1.2.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.
- package/LICENSE +21 -0
- package/README.md +133 -2
- package/README.zh.md +134 -0
- package/client.js +1662 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +20 -0
- package/index.js +1813 -0
- package/locale/en.json +116 -0
- package/locale/zh.json +116 -0
- package/package.json +70 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Laivikto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,134 @@
|
|
|
1
|
-
#
|
|
1
|
+
# SSH Cloud Servers
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Manage SSH servers from the DSH Desktop right sidebar: custom names, saved key or password credentials, and IPv4 / IPv6 / dual-stack display. After connecting, the card updates in place and an SSH terminal is established, and a task prompt is copied to your clipboard so you can paste it wherever you like. A companion tab browses remote files over a separate SSH channel.
|
|
4
|
+
|
|
5
|
+
English | [简体中文](./README.zh.md)
|
|
6
|
+
|
|
7
|
+
仓库:<https://github.com/Laivikto/dsh-ssh-servers>
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Native private-key picker** — no more pasting paths by hand. Click **Browse…**, choose a folder, pick the file from the list, and the absolute path is filled in automatically.
|
|
12
|
+
- **Live connection health** — checked every 30 seconds; `✓ Connected` / `✗ Unresponsive` shown directly on the server card.
|
|
13
|
+
- **In-place state changes** — after connecting you stay on the panel; the button turns into **Open new chat** so you decide when to move.
|
|
14
|
+
- **Prompt auto-copied** — the task prompt lands in your system clipboard on connect, ready to paste into any chat or editor, without occupying the input box.
|
|
15
|
+
- **Remote file browser** — a second tab lists, previews (text / image / hex) and downloads files on the connected host. It uses a separate `ssh` exec channel, so browsing never types into your terminal.
|
|
16
|
+
- **IPv4 / IPv6 / dual-stack display** — addresses resolved and shown once saved, masked by default behind an eye toggle.
|
|
17
|
+
- **Host-key policies** — trust-on-first-use (TOFU) or strict verification against `known_hosts`.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
This is a local DSH bundle. Install it as follows.
|
|
22
|
+
|
|
23
|
+
### Option 1: Download from GitHub (recommended)
|
|
24
|
+
|
|
25
|
+
1. Download the archive:
|
|
26
|
+
<https://github.com/Laivikto/dsh-ssh-servers/archive/refs/heads/main.zip>
|
|
27
|
+
(or `git clone` it)
|
|
28
|
+
2. Extract anywhere, e.g. `D:\plugins\dsh-ssh-servers`
|
|
29
|
+
3. In DSH Desktop open **Settings → Plugins** and click **Add plugin**
|
|
30
|
+
4. Select the extracted directory
|
|
31
|
+
5. Restart DSH; an **SSH Cloud Servers** tab appears in the right sidebar
|
|
32
|
+
|
|
33
|
+
### Option 2: Local development link
|
|
34
|
+
|
|
35
|
+
Point the profile at the repository:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@laivikto/dsh-ssh-servers": "link:E:/path/to/dsh-ssh-servers"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Also add `"@laivikto/dsh-ssh-servers"` to `dsh.profile.bundles`, then restart DSH.
|
|
46
|
+
|
|
47
|
+
> Do not run `pnpm` manually or edit `cordis.patch.yml` — DSH's plugin manager handles those steps.
|
|
48
|
+
|
|
49
|
+
### Option 3: npm (future)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install @laivikto/dsh-ssh-servers
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Uninstall
|
|
56
|
+
|
|
57
|
+
Disable or remove the plugin under **Settings → Plugins**.
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
1. Open the **SSH Cloud Servers** panel from the SSH icon in the chat toolbar, the right-sidebar tab, or the guide entry.
|
|
62
|
+
2. Click **Add server** and fill in name, host, port, username, and auth mode.
|
|
63
|
+
- **Private key**: click **Browse…** to pick a folder, or paste an absolute path. The plugin hands the path to the system OpenSSH; it never copies key contents.
|
|
64
|
+
- **Password**: stored in the local DSH credential store, written only to the terminal on connect, never sent to the model.
|
|
65
|
+
3. Choose address display (IPv4 / IPv6 / dual-stack) and host-key policy, then save.
|
|
66
|
+
4. Click **Connect**. The card updates in place to `✓ Connected`, the button becomes **Open new chat**, and the review prompt is **copied to your clipboard**.
|
|
67
|
+
5. Use **Copy prompt** to copy it again at any time; **Open new chat** only opens the session — paste and send when you are ready.
|
|
68
|
+
|
|
69
|
+
## Panel states
|
|
70
|
+
|
|
71
|
+
| State | Meaning | Button |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| `Needs setup` | Credentials not saved yet | Connect (disabled) |
|
|
74
|
+
| `Credentials ready` | Saved but not connected | Connect |
|
|
75
|
+
| `✓ Connected` | SSH link healthy (confirmed by 30s polling) | Open new chat |
|
|
76
|
+
| `✗ Unresponsive` | Link dropped or terminal exited | Open new chat |
|
|
77
|
+
|
|
78
|
+
> Health checks are non-intrusive: they only read terminal process state and on-screen disconnect markers. **No command is ever written into your terminal.**
|
|
79
|
+
|
|
80
|
+
## Security boundaries
|
|
81
|
+
|
|
82
|
+
- Passwords and key passphrases live in the local DSH credential store — never in plaintext config, chat history, or tool arguments.
|
|
83
|
+
- Credential records are bound to `host + port + username + authMode`. Another local program calling the connect endpoint cannot redirect a stored password to a different **literal** target; DNS changes and first-connection host keys still require your own verification.
|
|
84
|
+
- Connections use an empty SSH config (`-F NUL` / `/dev/null`) and disable `ProxyCommand`, `ProxyJump`, `LocalCommand`, and `RemoteCommand`, so `~/.ssh/config` cannot rewrite the real target or run extra commands at connect time.
|
|
85
|
+
- Private keys are read only by the local `ssh` process via `-i`. The plugin does not decrypt, copy, or upload them. The key path is passed through a terminal environment variable and does not appear in echoed commands, though OpenSSH debug output or terminal history may still contain it. Key **contents** are never sent to the model.
|
|
86
|
+
- The key-file listing endpoint returns only name, path, size, and a "looks like a key" flag — **never file contents**. The remote file endpoints (`fs-list` / `fs-preview` / `fs-download`) do return remote content to the local panel, but only for an absolute path on a host whose saved credential you already own; they are reachable only from the trusted DSH page (loopback plus Origin / `Sec-Fetch-Site` checks) and never hand content to the model. All paths reject newline and null-byte injection and are single-quote escaped before reaching the remote shell.
|
|
87
|
+
- The remote file channel honours your host-key policy: **strict** mode passes `StrictHostKeyChecking=yes`, so an unknown host key fails instead of being silently accepted.
|
|
88
|
+
- The file channel spawns a resolved absolute `ssh` binary with `shell: false`, so a planted `ssh.exe` in the current directory cannot hijack it.
|
|
89
|
+
- Every remote command executed through the plugin requires confirmation in the UI. Each confirmation authorizes exactly one command.
|
|
90
|
+
- **The plugin cannot restrict other local tools in the chat.** If the chat also has local execution tools, the model may still attempt local commands. On connect, the plugin tries to install a tool guard allowing only `ssh_terminal_read`, `ssh_terminal_run`, web lookup, `skill`, and user confirmation; if the host version lacks support, only the prompt constrains the model. Key-file read permissions remain governed by your DSH permission mode and OS user.
|
|
91
|
+
- Trust-on-first-use cannot verify a server before the first connection; strict mode requires a correct `known_hosts` entry or the connection fails.
|
|
92
|
+
- The local DSH credential store file is readable by the current OS user. The plugin does not expose its path to the model, but local tools running as the same user could read it; this is not OS-keychain-grade isolation.
|
|
93
|
+
- Remote command output enters the chat context and may contain server details. Avoid running commands that print secrets on servers you do not trust.
|
|
94
|
+
|
|
95
|
+
## What it is not
|
|
96
|
+
|
|
97
|
+
- Not a standalone vulnerability scanner: no built-in CVE database, no automatic detection or remediation.
|
|
98
|
+
- Does not run checks in the background. Connecting only starts an SSH login; no remote inspection commands are executed.
|
|
99
|
+
- Does not generate SSH keys, do port forwarding, SFTP, or bastion/jump-host chaining.
|
|
100
|
+
|
|
101
|
+
## Requirements and limitations
|
|
102
|
+
|
|
103
|
+
- Windows requires the system OpenSSH client (`ssh.exe`).
|
|
104
|
+
- The native folder picker is available only in DSH Desktop (Windows / macOS); in a plain browser, **Browse…** tells you to enter the path manually.
|
|
105
|
+
- One chat tracks one SSH connection opened by this plugin; reconnecting replaces the previous agent terminal.
|
|
106
|
+
- Interactive second factors (keyboard-interactive / 2FA) must be completed manually in the right-side terminal.
|
|
107
|
+
- Strict host-key verification depends on the system `known_hosts`.
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Syntax check
|
|
113
|
+
npm run check
|
|
114
|
+
|
|
115
|
+
# Run tests (58 tests; DSH not required — API tests skip automatically)
|
|
116
|
+
npm test
|
|
117
|
+
|
|
118
|
+
# Watch mode
|
|
119
|
+
npm run test:watch
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Test layout:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
tests/
|
|
126
|
+
├── core.test.mjs host/port/key-path normalization, SSH command construction
|
|
127
|
+
├── connection.test.mjs auth waiting, failure detection, tool guard, command capture
|
|
128
|
+
├── features.test.mjs key detection, hidden-file filtering, sorting, disconnect, state mapping
|
|
129
|
+
└── api.test.mjs end-to-end API (port auto-detected, skipped when unavailable)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
[MIT](./LICENSE) © 2026 Laivikto
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# SSH 云服务器
|
|
2
|
+
|
|
3
|
+
在 DSH Desktop 的右侧栏管理 SSH 服务器:自定义名称、保存私钥路径或密码、选择显示 IPv4 / IPv6 / 双栈。点击「连接」后在本页就地显示连接状态并建立 SSH 终端,同时把任务提示自动复制到剪贴板,由你决定粘贴到哪里。配套的「SSH 服务文件」标签页通过独立 SSH 通道浏览远端文件。
|
|
4
|
+
|
|
5
|
+
[English](./README.md) | 简体中文
|
|
6
|
+
|
|
7
|
+
仓库:<https://github.com/Laivikto/dsh-ssh-servers>
|
|
8
|
+
|
|
9
|
+
## 功能
|
|
10
|
+
|
|
11
|
+
- **原生私钥文件选择器** — 不用手动粘贴路径。点「浏览…」选出目录,插件列出里面的文件,点一下自动填入。
|
|
12
|
+
- **实时连接健康** — 每 30 秒检测一次,`✓ 已连接` / `✗ 未响应` 直接显示在服务器卡片上。
|
|
13
|
+
- **就地状态切换** — 连接成功后停留在本页,按钮变为「打开新聊天」,由你决定何时进入。
|
|
14
|
+
- **提示词自动复制** — 连接成功即把任务提示写入系统剪贴板,可粘贴到任意聊天或编辑器;不占用输入框。
|
|
15
|
+
- **远端文件浏览** — 独立标签页列出、预览(文本 / 图片 / 十六进制)并下载已连接主机上的文件。走独立的 `ssh` 执行通道,浏览时不会往你的终端里输入命令。
|
|
16
|
+
- **IPv4 / IPv6 / 双栈显示** — 保存后自动解析并展示地址,默认打码,可用眼睛图标切换明文。
|
|
17
|
+
- **主机密钥策略** — 首次自动信任(TOFU)或依赖 `known_hosts` 的严格校验。
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
本插件是 DSH 的本地 bundle,安装方式如下。
|
|
22
|
+
|
|
23
|
+
### 方式一:从 GitHub 下载(推荐)
|
|
24
|
+
|
|
25
|
+
1. 下载仓库压缩包:
|
|
26
|
+
<https://github.com/Laivikto/dsh-ssh-servers/archive/refs/heads/main.zip>
|
|
27
|
+
(也可以直接 `git clone`)
|
|
28
|
+
2. 解压到任意目录,例如 `D:\plugins\dsh-ssh-servers`
|
|
29
|
+
3. 打开 DSH Desktop,进入 **设置 → 插件**(Plugins),点击 **添加插件 / Add plugin**
|
|
30
|
+
4. 选择上一步解压出来的目录
|
|
31
|
+
5. 重启 DSH,右侧栏会出现「SSH 云服务器」标签
|
|
32
|
+
|
|
33
|
+
### 方式二:开发者本地联调
|
|
34
|
+
|
|
35
|
+
把仓库目录直接链接进 profile:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# profile 的 package.json 里声明
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@laivikto/dsh-ssh-servers": "link:E:/path/to/dsh-ssh-servers"
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
并在 `dsh.profile.bundles` 列表中加入 `"@laivikto/dsh-ssh-servers"`,然后重启 DSH。
|
|
45
|
+
|
|
46
|
+
> 不要手动跑 `pnpm` 或直接改 `cordis.patch.yml`;DSH 的插件管理器会负责这些步骤。
|
|
47
|
+
|
|
48
|
+
### 方式三:npm(未来)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 发布后可用
|
|
52
|
+
npm install @laivikto/dsh-ssh-servers
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 卸载
|
|
56
|
+
|
|
57
|
+
在 **设置 → 插件** 中禁用或移除该插件即可。
|
|
58
|
+
|
|
59
|
+
## 使用
|
|
60
|
+
|
|
61
|
+
1. 在聊天右上角的 SSH 图标,或右侧栏的标签页 / 向导入口中打开「SSH 云服务器」面板。
|
|
62
|
+
2. 点击「添加服务器」,填写名称、地址、端口、用户名和认证方式。
|
|
63
|
+
- **私钥方式**:点「浏览…」选择目录,或直接粘贴绝对路径。插件把路径交给系统 OpenSSH;不会复制私钥内容。
|
|
64
|
+
- **密码方式**:密码保存在本机 DSH 凭据库,点击连接时只写入终端,不进入聊天。
|
|
65
|
+
3. 选择地址显示(IPv4 / IPv6 / 双栈)和主机密钥策略,保存。
|
|
66
|
+
4. 点击「连接」。卡片就地显示 `✓ 已连接`,按钮变为「打开新聊天」,同时**检查提示已复制到剪贴板**。
|
|
67
|
+
5. 点「复制提示」可随时重新复制;点「打开新聊天」只是打开会话,粘贴提示后自行发送。
|
|
68
|
+
|
|
69
|
+
## 界面状态
|
|
70
|
+
|
|
71
|
+
| 状态 | 含义 | 按钮 |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| `待配置` | 尚未保存凭据 | 连接(禁用) |
|
|
74
|
+
| `凭据就绪` | 凭据已保存,未连接 | 连接 |
|
|
75
|
+
| `✓ 已连接` | SSH 链路正常(30 秒轮询确认) | 打开新聊天 |
|
|
76
|
+
| `✗ 未响应` | 链路已断或终端已退出 | 打开新聊天 |
|
|
77
|
+
|
|
78
|
+
> 健康检测是非侵入式的:只读取终端进程状态和屏幕断线特征,**不会向你的终端写入任何命令**。
|
|
79
|
+
|
|
80
|
+
## 安全边界
|
|
81
|
+
|
|
82
|
+
- 密码与私钥口令保存在 DSH 的本地凭据库中,不以明文写入配置或聊天记录,也不会作为工具参数发送给模型。
|
|
83
|
+
- 凭据记录与目标 `host + port + username + authMode` 绑定。即使本机其他程序调用连接接口,也不能把已保存的密码改发到另一个**字面目标**;DNS 解析变化或首次连接的主机密钥仍需你自行验证。
|
|
84
|
+
- 连接使用空 SSH 配置(`-F NUL` / `/dev/null`),并禁用 `ProxyCommand`、`ProxyJump`、`LocalCommand`、`RemoteCommand` 等配置项,避免 `~/.ssh/config` 改写实际目标或在连接阶段执行额外命令。
|
|
85
|
+
- 私钥文件只由本机 `ssh` 进程通过 `-i` 读取。插件不解密、不复制、不上传私钥;私钥路径通过终端环境变量传入,命令回显不显示路径,但 OpenSSH 调试输出或终端历史仍可能包含路径。私钥**内容**不会发送给模型。
|
|
86
|
+
- 私钥目录列举接口只返回文件名、路径、大小和「是否疑似私钥」,**不读取私钥内容**。远端文件接口(`fs-list` / `fs-preview` / `fs-download`)会把远端文件内容返回到本地面板,但仅限「你已保存凭据的主机 + 绝对路径」,且只能从受信任的 DSH 页面调用(loopback 加 Origin / `Sec-Fetch-Site` 校验),内容不会进入模型上下文。所有路径都拒绝换行与空字节注入,并在抵达远端 shell 前做单引号转义。
|
|
87
|
+
- 文件通道遵守你设置的主机密钥策略:**严格模式**会传 `StrictHostKeyChecking=yes`,未知主机密钥直接失败,而不是被静默接受。
|
|
88
|
+
- 文件通道用解析出的 `ssh` 绝对路径并以 `shell: false` 启动,当前目录里被放置伪装的 `ssh.exe` 也无法劫持。
|
|
89
|
+
- 每条通过插件执行的远程命令都要在界面中确认。`ssh_terminal_run` 的确认按钮只授权当前这一条命令。
|
|
90
|
+
- **插件不能限制聊天里的其他本地工具**:如果该聊天同时拥有本机可执行工具,模型仍可能尝试本地命令。建立连接后,插件会尝试为该 Agent 安装工具限制,只允许 `ssh_terminal_read`、`ssh_terminal_run`、联网查证、`skill` 和用户确认;若宿主版本不支持该限制,则只剩提示词约束。私钥文件读取权限最终由你的 DSH 权限模式和操作系统用户决定。
|
|
91
|
+
- 首次自动信任(TOFU)无法在第一次连接前验证服务器身份;严格模式要求本机 `known_hosts` 已有正确记录,否则连接失败。
|
|
92
|
+
- 本机 DSH 凭据库文件对当前操作系统用户可读。插件不向模型提供该路径,但同一用户运行的本地工具仍可能读到它;这不是操作系统钥匙串级别的隔离。
|
|
93
|
+
- 远端命令输出会进入聊天上下文,可能包含服务器信息。不要在不信任的服务器上运行会输出秘密的命令。
|
|
94
|
+
|
|
95
|
+
## 它不是
|
|
96
|
+
|
|
97
|
+
- 不是独立的漏洞扫描器,不内置 CVE 数据库,也不会自动检测或自动修复。
|
|
98
|
+
- 不会在后台自动运行检查。连接本身只启动 SSH 登录,不执行远端检查命令。
|
|
99
|
+
- 不负责生成 SSH 密钥、端口转发、SFTP 或跳板机。
|
|
100
|
+
|
|
101
|
+
## 依赖与限制
|
|
102
|
+
|
|
103
|
+
- Windows 需要系统 OpenSSH 客户端(`ssh.exe`)。
|
|
104
|
+
- 原生目录选择器仅在 DSH Desktop(Windows / macOS)可用;纯浏览器环境下「浏览…」会提示改用手动输入路径。
|
|
105
|
+
- 一个聊天只跟踪一条由本插件建立的 SSH 连接;重新连接会替换旧的 Agent 终端。
|
|
106
|
+
- 需要交互式二次验证(如键盘交互 / 2FA)时,请在右侧终端中手动完成。
|
|
107
|
+
- 严格主机密钥校验依赖系统 `known_hosts`。
|
|
108
|
+
|
|
109
|
+
## 开发
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# 语法检查
|
|
113
|
+
npm run check
|
|
114
|
+
|
|
115
|
+
# 运行测试(58 项,无需 DSH 运行;API 测试会自动跳过)
|
|
116
|
+
npm test
|
|
117
|
+
|
|
118
|
+
# 监听模式
|
|
119
|
+
npm run test:watch
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
测试结构:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
tests/
|
|
126
|
+
├── core.test.mjs 主机/端口/密钥路径规范化、SSH 命令构造
|
|
127
|
+
├── connection.test.mjs 认证等待、失败识别、工具守卫、命令捕获
|
|
128
|
+
├── features.test.mjs 私钥识别、隐藏文件过滤、排序、断线判定、状态映射
|
|
129
|
+
└── api.test.mjs 端到端 API(自动探测端口,不可用时跳过)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 许可证
|
|
133
|
+
|
|
134
|
+
[MIT](./LICENSE) © 2026 Laivikto
|