@kin-tio/cli 0.6.1 → 0.7.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/.env.example +4 -2
- package/CHANGELOG.md +39 -0
- package/README.md +55 -16
- package/README.zh-CN.md +36 -10
- package/assets/avatar.svg +32 -0
- package/assets/logo.svg +22 -0
- package/dist/src/cli.js +245 -4
- package/dist/src/config.js +72 -17
- package/dist/src/ilink/cli-accounts.js +66 -0
- package/dist/src/ilink/cli-login.js +563 -0
- package/dist/src/ilink/cli-start.js +57 -0
- package/dist/src/ilink/enrollment.js +24 -0
- package/dist/src/ilink/login-manager.js +102 -30
- package/dist/src/ilink/login-store.js +78 -29
- package/dist/src/ilink/qr.js +67 -3
- package/dist/src/ilink/secret-box.js +73 -0
- package/dist/src/ilink/sqlite-store.js +211 -13
- package/dist/src/mcp/ilink-login-server.js +160 -0
- package/dist/src/mcp/ipc-host.js +4 -1
- package/dist/src/mcp/ipc-protocol.js +22 -0
- package/dist/src/runtime.js +226 -92
- package/dist/src/services/codex-agent.js +57 -27
- package/dist/src/services/codex-app-server.js +4 -2
- package/dist/src/services/conversation-processor.js +8 -2
- package/dist/src/state/sqlite-store.js +151 -10
- package/dist/src/version.js +1 -1
- package/package.json +3 -1
package/.env.example
CHANGED
|
@@ -28,8 +28,10 @@ WECOM_AUTH_CONFIRMATION=Code accepted. You can continue the conversation.
|
|
|
28
28
|
# WECOM_MCP_OBSERVE_MS=5000
|
|
29
29
|
# SHUTDOWN_TIMEOUT_MS=10000
|
|
30
30
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
31
|
+
# Enable Weixin iLink inside the combined `kintio start` runtime. Standalone
|
|
32
|
+
# `kintio ilink login` and `kintio ilink start` do not require this flag.
|
|
33
|
+
# Generate a 32-byte base64url key and keep it in the deployment secret store;
|
|
34
|
+
# Bot and context tokens are encrypted with it.
|
|
33
35
|
ILINK_ENABLED=false
|
|
34
36
|
# Prefer a deployment-secret value. If omitted, the service creates a private
|
|
35
37
|
# 0600 key file beside SQLite; never register that key with a chat Agent.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,45 @@ This file records important user-visible changes after the first public release.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.7.0
|
|
8
|
+
|
|
9
|
+
- Added `kintio ilink login`, which reuses the iLink enrollment state machine
|
|
10
|
+
while rendering its five-minute QR code directly in an interactive terminal;
|
|
11
|
+
no WeChat KF conversation or Agent turn is required. Accounts
|
|
12
|
+
enrolled locally receive host-level Agent access and inherit the host runtime
|
|
13
|
+
configuration, while remotely offered iLink accounts remain restricted
|
|
14
|
+
([#54](https://github.com/Gkxie/kintio/issues/54)).
|
|
15
|
+
- Added an explicit `--qr-output <file>` view for `kintio ilink login`, allowing
|
|
16
|
+
graphical and non-terminal callers to consume a temporary raw PNG directly
|
|
17
|
+
from the QR payload without parsing ANSI terminal output. The file is created
|
|
18
|
+
exclusively and removed when the login attempt ends
|
|
19
|
+
([#57](https://github.com/Gkxie/kintio/issues/57)).
|
|
20
|
+
- Made iLink a standalone lifecycle: `kintio ilink login` now initializes and
|
|
21
|
+
persists an account without setup, an environment file, Hono, or a running
|
|
22
|
+
Worker, while safely delegating to a running instance when present;
|
|
23
|
+
`kintio ilink start` starts polling and the host Agent in the foreground without
|
|
24
|
+
a public HTTP listener. The iLink Runtime configuration no longer contains a
|
|
25
|
+
synthetic WeChat KF adapter
|
|
26
|
+
([#59](https://github.com/Gkxie/kintio/issues/59)).
|
|
27
|
+
- Added per-account `kintio ilink list`, `start`, `stop`, and confirmed `delete`
|
|
28
|
+
lifecycle commands. One Runtime can reconcile multiple selected listeners;
|
|
29
|
+
complete deletion atomically purges the selected account and all Kintio data
|
|
30
|
+
scoped to it while preserving unrelated accounts and channels
|
|
31
|
+
([#60](https://github.com/Gkxie/kintio/issues/60)).
|
|
32
|
+
|
|
33
|
+
## 0.6.2
|
|
34
|
+
|
|
35
|
+
- Added a custom Kintio wordmark and its circular-safe TIO avatar to the English
|
|
36
|
+
and Simplified Chinese entry pages and public package.
|
|
37
|
+
- Added a repository-scoped Release App that deterministically maintains one
|
|
38
|
+
fully checked Release PR from the reviewed Unreleased notes; maintainers now
|
|
39
|
+
authorize a release by reviewing and merging that PR only
|
|
40
|
+
([#48](https://github.com/Gkxie/kintio/issues/48)).
|
|
41
|
+
- Made a merged, owner-authored Release PR the sole human release authorization;
|
|
42
|
+
Kintio now creates the annotated tag and dispatches its verified OIDC release
|
|
43
|
+
automatically
|
|
44
|
+
([#45](https://github.com/Gkxie/kintio/issues/45)).
|
|
45
|
+
|
|
7
46
|
## 0.6.1 - 2026-08-31
|
|
8
47
|
|
|
9
48
|
- Added approval-gated npm Trusted Publishing with an exact artifact integrity
|
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<h1>
|
|
4
|
+
<img src="assets/logo.svg" alt="Kintio" width="320" />
|
|
5
|
+
</h1>
|
|
4
6
|
|
|
5
7
|
**Connect chat channels to an Agent you control.**
|
|
6
8
|
|
|
@@ -65,33 +67,67 @@ Prerequisites:
|
|
|
65
67
|
|
|
66
68
|
```bash
|
|
67
69
|
npm install --global @kin-tio/cli
|
|
68
|
-
kintio setup
|
|
69
70
|
codex login status
|
|
70
71
|
```
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
skill, and writes the channel configuration template. POSIX systems use mode `0600`;
|
|
74
|
-
Windows keeps the instance inside the current user's profile. No adapter is enabled
|
|
75
|
-
by default. Follow the [setup guide](https://github.com/Gkxie/kintio/blob/master/docs/setup.md)
|
|
76
|
-
and edit `~/.kintio/.env` to configure one adapter:
|
|
73
|
+
For an iLink-only instance, no setup file or public HTTP listener is required:
|
|
77
74
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
currently starts from an authorized WeChat KF conversation.
|
|
75
|
+
```bash
|
|
76
|
+
kintio ilink login
|
|
77
|
+
kintio ilink start
|
|
78
|
+
```
|
|
83
79
|
|
|
84
|
-
|
|
80
|
+
`ilink login` performs one encrypted enrollment, starts no listener, and exits. `ilink start` then runs provider
|
|
81
|
+
polling and the host Agent in the foreground without Hono or a TCP listener. Both commands
|
|
82
|
+
use `~/.kintio` by default and accept `--home`. With multiple accounts, use `ilink list`
|
|
83
|
+
and pass the displayed provider ID or account key through `--account`. Repeated `start`
|
|
84
|
+
commands add accounts to the live runtime; `stop` removes one.
|
|
85
|
+
|
|
86
|
+
For a callback-based adapter, create and edit the deployment configuration instead:
|
|
85
87
|
|
|
86
88
|
```bash
|
|
89
|
+
kintio setup
|
|
87
90
|
kintio start
|
|
88
91
|
kintio status
|
|
89
92
|
kintio logs --lines 100
|
|
90
93
|
```
|
|
91
94
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
`kintio setup` creates a private instance under `~/.kintio`, installs the managed Agent
|
|
96
|
+
skill, and writes the channel configuration template. Follow the
|
|
97
|
+
[setup guide](https://github.com/Gkxie/kintio/blob/master/docs/setup.md):
|
|
98
|
+
|
|
99
|
+
- For WeChat KF API, set its callback token, EncodingAESKey, CorpID, and secret. A temporary
|
|
100
|
+
`WECOM_AUTH_TRIGGER` can authorize the first user without knowing their
|
|
101
|
+
`external_userid` in advance.
|
|
102
|
+
- A combined callback + iLink deployment may additionally set `ILINK_ENABLED=true`.
|
|
103
|
+
|
|
104
|
+
For a graphical or non-terminal caller, select a temporary raw PNG instead of ANSI blocks:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
kintio ilink login --qr-output ~/.kintio/ilink-login.png
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The target must be directly inside the selected Kintio instance directory and must not
|
|
111
|
+
already exist. Kintio removes the PNG when login succeeds, expires, is cancelled, or fails;
|
|
112
|
+
the QR payload is never printed. Without `--qr-output`, the command
|
|
113
|
+
requires an interactive terminal. Both forms stop waiting after five minutes and never
|
|
114
|
+
start an Agent turn. The resulting iLink identity represents
|
|
115
|
+
the local operator and inherits the host Agent configuration without Kintio's untrusted-
|
|
116
|
+
channel capability restrictions. Show this QR code only to someone authorized to control
|
|
117
|
+
the host Agent. Run `kintio ilink start` after enrollment to process messages without Hono.
|
|
118
|
+
|
|
119
|
+
To permanently remove an account and every Kintio record scoped to it, use:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
kintio ilink delete --account <provider-id-or-account-key> --yes
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The explicit confirmation is mandatory. Credentials, conversations, messages, media,
|
|
126
|
+
delivery records, and enrollment audit rows for that account are deleted atomically.
|
|
127
|
+
|
|
128
|
+
For callback deployments, confirm that `kintio logs` contains
|
|
129
|
+
`Hono server is listening on port 8888`. Use `kintio run` when a foreground process is
|
|
130
|
+
preferable to the native daemon.
|
|
95
131
|
Existing source-based deployments can keep their current state after the one-time
|
|
96
132
|
process-manager migration described in the setup guide.
|
|
97
133
|
|
|
@@ -109,6 +145,9 @@ Source builds and contributor setup are documented in
|
|
|
109
145
|
Hono route; every action still requires a short-lived conversation capability.
|
|
110
146
|
- Project-level Agent capability restrictions are not an operating-system sandbox. Use a
|
|
111
147
|
dedicated system account and additional isolation appropriate to the Agent's real powers.
|
|
148
|
+
- An iLink account enrolled by `kintio ilink login` is explicitly host-authorized; its owner
|
|
149
|
+
receives the capabilities allowed by the host Agent configuration. Accounts enrolled from
|
|
150
|
+
a remote adapter remain restricted, and chat input cannot change this persisted trust level.
|
|
112
151
|
- A provider accepting an outbound request does not prove that a client displayed it;
|
|
113
152
|
uncertain outcomes remain explicit to avoid duplicate delivery.
|
|
114
153
|
|
package/README.zh-CN.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<h1>
|
|
4
|
+
<img src="assets/logo.svg" alt="Kintio" width="320" />
|
|
5
|
+
</h1>
|
|
4
6
|
|
|
5
7
|
**把聊天通道连接到你掌控的 Agent。**
|
|
6
8
|
|
|
@@ -41,25 +43,49 @@
|
|
|
41
43
|
|
|
42
44
|
```bash
|
|
43
45
|
npm install --global @kin-tio/cli
|
|
44
|
-
kintio setup
|
|
45
46
|
codex login status
|
|
46
47
|
```
|
|
47
48
|
|
|
48
|
-
`
|
|
49
|
-
模板 `.env`。macOS/Linux 使用 `0600`,Windows 则限定在当前用户目录的 ACL
|
|
50
|
-
边界内。默认不启用任何适配器;请按英文
|
|
51
|
-
[部署指南](https://github.com/Gkxie/kintio/blob/master/docs/setup.md)配置 WeChat KF API,或为已有 Weixin iLink 绑定设置
|
|
52
|
-
`ILINK_ENABLED=true`。
|
|
49
|
+
iLink 可以完全独立使用,不需要 `setup`、`.env` 或公网 HTTP:
|
|
53
50
|
|
|
54
51
|
```bash
|
|
52
|
+
kintio ilink login
|
|
53
|
+
kintio ilink start
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`ilink login` 完成一次扫码、加密保存凭据后退出,不会自行启动监听;`ilink start` 不启动 Hono 或 TCP 端口,
|
|
57
|
+
只以前台方式运行 iLink 长轮询和宿主 Agent。两者默认使用 `~/.kintio`。
|
|
58
|
+
存在多个账号时,先用 `kintio ilink list` 查看账号,再通过 `--account` 指定
|
|
59
|
+
`start`、`stop` 或 `delete` 的目标;正在运行时可继续执行 `start` 增加监听账号。
|
|
60
|
+
|
|
61
|
+
需要部署公网回调渠道时,再使用:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
kintio setup
|
|
55
65
|
kintio start
|
|
56
66
|
kintio status
|
|
57
67
|
kintio logs --lines 100
|
|
58
68
|
```
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
具体配置见英文[部署指南](https://github.com/Gkxie/kintio/blob/master/docs/setup.md)。
|
|
71
|
+
|
|
72
|
+
图形界面或非交互调用方可以显式选择临时的原始 PNG,而不是解析终端字符:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
kintio ilink login --qr-output ~/.kintio/ilink-login.png
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
目标文件必须直接位于所选 Kintio 实例目录中且不能预先存在;登录成功、过期、取消或失败后,Kintio 会自动删除该文件,并且
|
|
79
|
+
不会打印二维码原始内容。二维码五分钟后过期;该命令不会唤醒 Agent。通过本机命令建立的 iLink 身份代表宿主机
|
|
80
|
+
所有者的明确授权,后续对话直接继承宿主 Agent 配置,不再套用不可信渠道的能力限制。
|
|
81
|
+
只应让获准控制宿主 Agent 的人扫描该二维码。登录后运行 `kintio ilink start` 即可在不
|
|
82
|
+
启动 Hono 的情况下处理消息。
|
|
83
|
+
|
|
84
|
+
`kintio ilink delete --account <账号> --yes` 会不可恢复地删除该账号及其在 Kintio
|
|
85
|
+
中的凭据、会话、消息、媒体、发送记录和登录审计;`--yes` 为强制确认参数。
|
|
86
|
+
|
|
87
|
+
公网回调部署启动后,应确认 `kintio logs` 包含
|
|
88
|
+
`Hono server is listening on port 8888`。
|
|
63
89
|
源码构建与贡献者开发环境见英文
|
|
64
90
|
[贡献指南](https://github.com/Gkxie/kintio/blob/master/CONTRIBUTING.md)。
|
|
65
91
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3
|
+
width="200"
|
|
4
|
+
height="200"
|
|
5
|
+
viewBox="190 15 190 190"
|
|
6
|
+
role="img"
|
|
7
|
+
aria-labelledby="kintio-avatar-title"
|
|
8
|
+
aria-describedby="kintio-avatar-description"
|
|
9
|
+
>
|
|
10
|
+
<!-- Letterform foundation: Manrope by Mikhail Sharanda, SIL Open Font License 1.1. -->
|
|
11
|
+
<title id="kintio-avatar-title">Kintio TIO avatar</title>
|
|
12
|
+
<desc id="kintio-avatar-description">A circular Kinetic Panels background with the Kintio TIO return aperture.</desc>
|
|
13
|
+
<defs>
|
|
14
|
+
<clipPath id="kintio-kinetic-panels-circle">
|
|
15
|
+
<circle cx="285" cy="110" r="95"/>
|
|
16
|
+
</clipPath>
|
|
17
|
+
</defs>
|
|
18
|
+
<g clip-path="url(#kintio-kinetic-panels-circle)">
|
|
19
|
+
<rect x="190" y="15" width="190" height="190" fill="#17152B"/>
|
|
20
|
+
<polygon points="190,15 304,15 270,58 190,88" fill="#5268FF"/>
|
|
21
|
+
<polygon points="302,152 380,112 380,205 270,205" fill="#3B2F73"/>
|
|
22
|
+
<polygon points="190,88 214,78 214,104 190,116" fill="#8C78FF"/>
|
|
23
|
+
<g transform="translate(285 110) scale(0.86) translate(-285 -110)">
|
|
24
|
+
<path d="M766 0Q661 -20 561 -17Q461 -14 382 21Q303 56 262 132Q226 201 224 272Q222 343 222 433L222 1380L447 1380L447 445Q447 380 448.5 331.5Q450 283 469 251Q505 190 583.5 182.5Q662 175 766 190ZM20 902L20 1080L766 1080L766 902Z" fill="#F6F3EE" transform="translate(204.393 150) scale(0.063 -0.063)"/>
|
|
25
|
+
<path d="M160 1251L160 1463L385 1463L385 1251ZM160 0L160 1080L385 1080L385 0Z" fill="#FF7656" transform="translate(257.289 150) scale(0.063 -0.063)"/>
|
|
26
|
+
<path d="M613 -30Q451 -30 331 43Q211 116 145.5 244.5Q80 373 80 541Q80 710 147 838.5Q214 967 334 1038.5Q454 1110 613 1110Q776 1110 896 1037Q1016 964 1082 836Q1148 708 1148 541Q1148 372 1081.5 243.5Q1015 115 894.5 42.5Q774 -30 613 -30ZM613 182Q762 182 835.5 282Q909 382 909 541Q909 704 835 801.5Q761 899 613 899Q512 899 447 853.5Q382 808 350.5 727.5Q319 647 319 541Q319 377 393.5 279.5Q468 182 613 182Z" fill="#F6F3EE" transform="translate(290.624 150) scale(0.063 -0.063)"/>
|
|
27
|
+
<rect x="286" y="108" width="23" height="18" fill="#17152B"/>
|
|
28
|
+
<rect x="289" y="113" width="11" height="8" fill="#FF7656"/>
|
|
29
|
+
</g>
|
|
30
|
+
</g>
|
|
31
|
+
</svg>
|
|
32
|
+
|
package/assets/logo.svg
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3
|
+
width="374"
|
|
4
|
+
height="142"
|
|
5
|
+
viewBox="12.757 33.831 374.191 142.059"
|
|
6
|
+
role="img"
|
|
7
|
+
aria-labelledby="kintio-wordmark-title"
|
|
8
|
+
aria-describedby="kintio-wordmark-description"
|
|
9
|
+
>
|
|
10
|
+
<!-- Letterform foundation: Manrope by Mikhail Sharanda, SIL Open Font License 1.1. -->
|
|
11
|
+
<title id="kintio-wordmark-title">Kintio</title>
|
|
12
|
+
<desc id="kintio-wordmark-description">Kintio wordmark with an Ember i and an open return aperture.</desc>
|
|
13
|
+
<rect x="12.757" y="33.831" width="374.191" height="142.059" fill="#211920"/>
|
|
14
|
+
<path d="M139 0L140 1440L368 1440L368 560L778 1080L1061 1080L628 540L1102 0L801 0L368 520L368 0Z" fill="#F6F3EE" transform="translate(28.000 150) scale(0.063000 -0.063000)"/>
|
|
15
|
+
<path d="M160 1251L160 1463L385 1463L385 1251ZM160 0L160 1080L385 1080L385 0Z" fill="#F6F3EE" transform="translate(93.906 150) scale(0.063000 -0.063000)"/>
|
|
16
|
+
<path d="M893 0L893 526Q893 582 884 646.5Q875 711 847.5 768.5Q820 826 768 862Q716 898 630 898Q579 898 531.5 881Q484 864 447 825.5Q410 787 388.5 721.5Q367 656 367 559L230 614Q230 756 284.5 868.5Q339 981 442.5 1045.5Q546 1110 695 1110Q810 1110 887.5 1072Q965 1034 1011.5 973.5Q1058 913 1081.5 844Q1105 775 1113 710.5Q1121 646 1121 602L1121 0ZM139 0L139 1080L340 1080L340 757L367 757L367 0Z" fill="#F6F3EE" transform="translate(127.241 150) scale(0.063000 -0.063000)"/>
|
|
17
|
+
<path d="M766 0Q661 -20 561 -17Q461 -14 382 21Q303 56 262 132Q226 201 224 272Q222 343 222 433L222 1380L447 1380L447 445Q447 380 448.5 331.5Q450 283 469 251Q505 190 583.5 182.5Q662 175 766 190ZM20 902L20 1080L766 1080L766 902Z" fill="#F6F3EE" transform="translate(204.393 150) scale(0.063000 -0.063000)"/>
|
|
18
|
+
<path d="M160 1251L160 1463L385 1463L385 1251ZM160 0L160 1080L385 1080L385 0Z" fill="#FF7656" transform="translate(257.289 150) scale(0.063000 -0.063000)"/>
|
|
19
|
+
<path d="M613 -30Q451 -30 331 43Q211 116 145.5 244.5Q80 373 80 541Q80 710 147 838.5Q214 967 334 1038.5Q454 1110 613 1110Q776 1110 896 1037Q1016 964 1082 836Q1148 708 1148 541Q1148 372 1081.5 243.5Q1015 115 894.5 42.5Q774 -30 613 -30ZM613 182Q762 182 835.5 282Q909 382 909 541Q909 704 835 801.5Q761 899 613 899Q512 899 447 853.5Q382 808 350.5 727.5Q319 647 319 541Q319 377 393.5 279.5Q468 182 613 182Z" fill="#F6F3EE" transform="translate(290.624 150) scale(0.063000 -0.063000)"/>
|
|
20
|
+
<rect x="286" y="108" width="23" height="18" fill="#211920"/>
|
|
21
|
+
<rect x="289" y="113" width="11" height="8" fill="#FF7656"/>
|
|
22
|
+
</svg>
|
package/dist/src/cli.js
CHANGED
|
@@ -5,9 +5,12 @@ import path from 'node:path';
|
|
|
5
5
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
7
|
import crossSpawn from 'cross-spawn';
|
|
8
|
-
import { DAEMON_STOP_TIMEOUT_MS, loadConfig, parseStartTimeout, resolveProjectRoot, WORKER_GRACEFUL_TIMEOUT_MS, } from './config.js';
|
|
8
|
+
import { DAEMON_STOP_TIMEOUT_MS, loadConfig, loadIlinkEnrollmentConfig, loadIlinkRuntimeConfig, parseStartTimeout, resolveProjectRoot, WORKER_GRACEFUL_TIMEOUT_MS, } from './config.js';
|
|
9
9
|
import { isPathInside, samePath } from './lib/path-identity.js';
|
|
10
10
|
import { assertTrustedDirectory, ensureContainedDirectory, ensurePrivateDirectory, } from './lib/private-directory.js';
|
|
11
|
+
import { runIlinkCliLogin } from './ilink/cli-login.js';
|
|
12
|
+
import { runIlinkAccountCommand } from './ilink/cli-accounts.js';
|
|
13
|
+
import { startIlinkCliRuntime } from './ilink/cli-start.js';
|
|
11
14
|
import { daemonRecordPath, readDaemonRecord, requestControl, } from './runtime/daemon-protocol.js';
|
|
12
15
|
import { acquireSingleInstanceLock, processIsAlive, SingleInstanceLockError, } from './runtime/single-instance-lock.js';
|
|
13
16
|
import { installManagedSkill } from './runtime/managed-skill.js';
|
|
@@ -22,6 +25,11 @@ Commands:
|
|
|
22
25
|
restart Restart Kintio with the current installation and config
|
|
23
26
|
status Show the background process status
|
|
24
27
|
logs Follow Kintio logs
|
|
28
|
+
ilink login [options] Connect an iLink account with a QR code
|
|
29
|
+
ilink list List enrolled iLink accounts
|
|
30
|
+
ilink start [options] Start one iLink account without Hono
|
|
31
|
+
ilink stop [options] Stop one iLink account
|
|
32
|
+
ilink delete [options] Permanently delete one iLink account and its data
|
|
25
33
|
|
|
26
34
|
Options:
|
|
27
35
|
--home <directory> Instance directory (default: ~/.kintio)
|
|
@@ -30,7 +38,132 @@ Options:
|
|
|
30
38
|
--no-follow Print logs without following
|
|
31
39
|
-h, --help Show this help
|
|
32
40
|
-v, --version Show the Kintio version
|
|
41
|
+
|
|
42
|
+
Run "kintio ilink --help" for iLink account commands.
|
|
43
|
+
`;
|
|
44
|
+
const ILINK_LOGIN_HELP = `Usage: kintio ilink login [options]
|
|
45
|
+
|
|
46
|
+
Connect one iLink account, save its encrypted credentials, and exit. This
|
|
47
|
+
command does not require setup, an environment file, Hono, or a running Kintio
|
|
48
|
+
instance. By default, the QR code is rendered directly in an interactive
|
|
49
|
+
terminal and expires after five minutes.
|
|
50
|
+
|
|
51
|
+
The PNG option is required when stdout is not an interactive terminal. Whoever
|
|
52
|
+
scans this locally issued QR receives the capabilities allowed by the host Agent
|
|
53
|
+
configuration; show it only to an authorized operator.
|
|
54
|
+
|
|
55
|
+
Options:
|
|
56
|
+
--qr-output <file> Write a temporary raw QR PNG instead of terminal blocks
|
|
57
|
+
The file must be directly inside the instance directory
|
|
58
|
+
The file is removed when the login attempt ends
|
|
59
|
+
--home <directory> Instance directory (default: ~/.kintio)
|
|
60
|
+
--config <file> Optional environment overrides
|
|
61
|
+
-h, --help Show this help
|
|
62
|
+
`;
|
|
63
|
+
const ILINK_START_HELP = `Usage: kintio ilink start [options]
|
|
64
|
+
|
|
65
|
+
Run iLink long polling and the host Agent in the foreground without starting
|
|
66
|
+
Hono or opening a TCP listener. This command does not require setup or an
|
|
67
|
+
environment file. One account is selected automatically; multiple accounts
|
|
68
|
+
require --account. While this runtime is active, additional start commands add
|
|
69
|
+
accounts to the same process.
|
|
70
|
+
|
|
71
|
+
Options:
|
|
72
|
+
--account <id> Provider account ID or Kintio account key
|
|
73
|
+
--home <directory> Instance directory (default: ~/.kintio)
|
|
74
|
+
--config <file> Optional environment overrides
|
|
75
|
+
-h, --help Show this help
|
|
76
|
+
`;
|
|
77
|
+
const ILINK_STOP_HELP = `Usage: kintio ilink stop [options]
|
|
78
|
+
|
|
79
|
+
Stop one iLink account. Stopping the last account also exits a foreground
|
|
80
|
+
"kintio ilink start" runtime. One account is selected automatically; multiple
|
|
81
|
+
accounts require --account.
|
|
82
|
+
|
|
83
|
+
Options:
|
|
84
|
+
--account <id> Provider account ID or Kintio account key
|
|
85
|
+
--home <directory> Instance directory (default: ~/.kintio)
|
|
86
|
+
--config <file> Optional environment overrides
|
|
87
|
+
-h, --help Show this help
|
|
88
|
+
`;
|
|
89
|
+
const ILINK_LIST_HELP = `Usage: kintio ilink list [options]
|
|
90
|
+
|
|
91
|
+
List enrolled iLink accounts and whether each account is currently running.
|
|
92
|
+
|
|
93
|
+
Options:
|
|
94
|
+
--home <directory> Instance directory (default: ~/.kintio)
|
|
95
|
+
--config <file> Optional environment overrides
|
|
96
|
+
-h, --help Show this help
|
|
97
|
+
`;
|
|
98
|
+
const ILINK_DELETE_HELP = `Usage: kintio ilink delete [options]
|
|
99
|
+
|
|
100
|
+
Permanently delete one iLink account and all Kintio data scoped to it,
|
|
101
|
+
including credentials, conversations, messages, media, send records, and
|
|
102
|
+
enrollment audit records. This operation cannot be undone.
|
|
103
|
+
|
|
104
|
+
Options:
|
|
105
|
+
--account <id> Provider account ID or Kintio account key
|
|
106
|
+
--yes Confirm permanent deletion
|
|
107
|
+
--home <directory> Instance directory (default: ~/.kintio)
|
|
108
|
+
--config <file> Optional environment overrides
|
|
109
|
+
-h, --help Show this help
|
|
110
|
+
`;
|
|
111
|
+
const ILINK_HELP = `Usage: kintio ilink <command>
|
|
112
|
+
|
|
113
|
+
Commands:
|
|
114
|
+
login [options] Connect an iLink account with a QR code
|
|
115
|
+
list List enrolled accounts
|
|
116
|
+
start [options] Start one account without Hono
|
|
117
|
+
stop [options] Stop one account
|
|
118
|
+
delete [options] Permanently delete one account and its data
|
|
119
|
+
|
|
120
|
+
Run "kintio ilink <command> --help" for command options.
|
|
33
121
|
`;
|
|
122
|
+
const ILINK_COMMANDS = new Set(['login', 'list', 'start', 'stop', 'delete']);
|
|
123
|
+
const COMMANDS = new Set([
|
|
124
|
+
'setup',
|
|
125
|
+
'start',
|
|
126
|
+
'run',
|
|
127
|
+
'stop',
|
|
128
|
+
'restart',
|
|
129
|
+
'status',
|
|
130
|
+
'logs',
|
|
131
|
+
'ilink',
|
|
132
|
+
]);
|
|
133
|
+
const ILINK_SIGNALS = process.platform === 'win32'
|
|
134
|
+
? ['SIGINT', 'SIGTERM']
|
|
135
|
+
: ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
136
|
+
function signalExitCode(signal) {
|
|
137
|
+
if (signal === 'SIGHUP')
|
|
138
|
+
return 129;
|
|
139
|
+
if (signal === 'SIGTERM')
|
|
140
|
+
return 143;
|
|
141
|
+
return 130;
|
|
142
|
+
}
|
|
143
|
+
async function runWithIlinkSignals(operation) {
|
|
144
|
+
const controller = new AbortController();
|
|
145
|
+
let interruptedBy;
|
|
146
|
+
const interrupt = (signal) => {
|
|
147
|
+
interruptedBy ||= signal;
|
|
148
|
+
controller.abort();
|
|
149
|
+
};
|
|
150
|
+
const listeners = ILINK_SIGNALS.map((signal) => ({
|
|
151
|
+
signal,
|
|
152
|
+
listener: () => interrupt(signal),
|
|
153
|
+
}));
|
|
154
|
+
for (const { signal, listener } of listeners)
|
|
155
|
+
process.once(signal, listener);
|
|
156
|
+
try {
|
|
157
|
+
const result = await operation(controller.signal);
|
|
158
|
+
return result === 130 && interruptedBy
|
|
159
|
+
? signalExitCode(interruptedBy)
|
|
160
|
+
: result;
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
for (const { signal, listener } of listeners)
|
|
164
|
+
process.off(signal, listener);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
34
167
|
function defaultExecute(request) {
|
|
35
168
|
return new Promise((resolve, reject) => {
|
|
36
169
|
const child = crossSpawn(request.file, [...request.args], {
|
|
@@ -108,6 +241,11 @@ function runtimeDefaults() {
|
|
|
108
241
|
launchDaemon: defaultLaunchDaemon,
|
|
109
242
|
stdout: (text) => process.stdout.write(text),
|
|
110
243
|
stderr: (text) => process.stderr.write(text),
|
|
244
|
+
stdoutIsTTY: Boolean(process.stdout.isTTY),
|
|
245
|
+
stdoutColumns: process.stdout.columns || 80,
|
|
246
|
+
ilinkLogin: runIlinkCliLogin,
|
|
247
|
+
ilinkAccount: runIlinkAccountCommand,
|
|
248
|
+
ilinkStart: startIlinkCliRuntime,
|
|
111
249
|
};
|
|
112
250
|
}
|
|
113
251
|
function resolveInputPath(value, cwd) {
|
|
@@ -508,6 +646,9 @@ export async function runCli(args, overrides = {}) {
|
|
|
508
646
|
config: { type: 'string' },
|
|
509
647
|
lines: { type: 'string' },
|
|
510
648
|
'no-follow': { type: 'boolean' },
|
|
649
|
+
'qr-output': { type: 'string' },
|
|
650
|
+
account: { type: 'string' },
|
|
651
|
+
yes: { type: 'boolean' },
|
|
511
652
|
help: { type: 'boolean', short: 'h' },
|
|
512
653
|
version: { type: 'boolean', short: 'v' },
|
|
513
654
|
},
|
|
@@ -517,18 +658,118 @@ export async function runCli(args, overrides = {}) {
|
|
|
517
658
|
return 0;
|
|
518
659
|
}
|
|
519
660
|
const command = parsed.positionals[0];
|
|
520
|
-
|
|
661
|
+
const subcommand = parsed.positionals[1];
|
|
662
|
+
if (!command) {
|
|
663
|
+
if (parsed.values['qr-output'] !== undefined) {
|
|
664
|
+
throw new Error('--qr-output is valid only for "kintio ilink login"');
|
|
665
|
+
}
|
|
521
666
|
runtime.stdout(HELP);
|
|
522
667
|
return 0;
|
|
523
668
|
}
|
|
524
|
-
if (
|
|
525
|
-
|
|
669
|
+
if (command === 'help') {
|
|
670
|
+
if (parsed.positionals.length !== 1) {
|
|
671
|
+
throw new Error(`Unexpected argument: ${subcommand}`);
|
|
672
|
+
}
|
|
673
|
+
runtime.stdout(HELP);
|
|
674
|
+
return 0;
|
|
675
|
+
}
|
|
676
|
+
if (!COMMANDS.has(command))
|
|
677
|
+
throw new Error(`Unknown command: ${command}`);
|
|
678
|
+
if (command === 'ilink') {
|
|
679
|
+
if (parsed.values.help && parsed.positionals.length === 1) {
|
|
680
|
+
runtime.stdout(ILINK_HELP);
|
|
681
|
+
return 0;
|
|
682
|
+
}
|
|
683
|
+
if (!subcommand || !ILINK_COMMANDS.has(subcommand) ||
|
|
684
|
+
parsed.positionals.length !== 2) {
|
|
685
|
+
throw new Error('Usage: kintio ilink <login|list|start|stop|delete>');
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
else if (parsed.positionals.length !== 1) {
|
|
689
|
+
throw new Error(`Unexpected argument: ${subcommand}`);
|
|
690
|
+
}
|
|
691
|
+
if (parsed.values.help) {
|
|
692
|
+
runtime.stdout(command !== 'ilink'
|
|
693
|
+
? HELP
|
|
694
|
+
: subcommand === 'login' ? ILINK_LOGIN_HELP
|
|
695
|
+
: subcommand === 'list' ? ILINK_LIST_HELP
|
|
696
|
+
: subcommand === 'start' ? ILINK_START_HELP
|
|
697
|
+
: subcommand === 'stop' ? ILINK_STOP_HELP
|
|
698
|
+
: ILINK_DELETE_HELP);
|
|
699
|
+
return 0;
|
|
526
700
|
}
|
|
527
701
|
if (command !== 'logs' &&
|
|
528
702
|
(parsed.values.lines !== undefined || parsed.values['no-follow'])) {
|
|
529
703
|
throw new Error('--lines and --no-follow are valid only for "kintio logs"');
|
|
530
704
|
}
|
|
705
|
+
if ((command !== 'ilink' || subcommand !== 'login') &&
|
|
706
|
+
parsed.values['qr-output'] !== undefined) {
|
|
707
|
+
throw new Error('--qr-output is valid only for "kintio ilink login"');
|
|
708
|
+
}
|
|
709
|
+
if (parsed.values['qr-output'] === '') {
|
|
710
|
+
throw new Error('--qr-output requires a non-empty file path');
|
|
711
|
+
}
|
|
712
|
+
if (parsed.values.account !== undefined &&
|
|
713
|
+
(command !== 'ilink' || !['start', 'stop', 'delete'].includes(subcommand || ''))) {
|
|
714
|
+
throw new Error('--account is valid only for "kintio ilink start|stop|delete"');
|
|
715
|
+
}
|
|
716
|
+
if (parsed.values.account === '') {
|
|
717
|
+
throw new Error('--account requires a non-empty account ID or key');
|
|
718
|
+
}
|
|
719
|
+
if (parsed.values.yes && (command !== 'ilink' || subcommand !== 'delete')) {
|
|
720
|
+
throw new Error('--yes is valid only for "kintio ilink delete"');
|
|
721
|
+
}
|
|
531
722
|
const location = instanceLocation(parsed.values, runtime);
|
|
723
|
+
const qrOutputPath = parsed.values['qr-output'] === undefined
|
|
724
|
+
? undefined
|
|
725
|
+
: resolveInputPath(parsed.values['qr-output'], runtime.cwd);
|
|
726
|
+
if (qrOutputPath && !samePath(path.dirname(qrOutputPath), location.home)) {
|
|
727
|
+
throw new Error('iLink QR output must be directly inside the instance directory');
|
|
728
|
+
}
|
|
729
|
+
if (command === 'ilink') {
|
|
730
|
+
if (privateFile(location.configFile, 'Kintio config')) {
|
|
731
|
+
assertTrustedDirectory(path.dirname(location.configFile), 'Kintio config directory', false);
|
|
732
|
+
}
|
|
733
|
+
prepareDirectories(location.home);
|
|
734
|
+
return await runWithIlinkSignals(async (signal) => {
|
|
735
|
+
const enrollmentConfig = loadIlinkEnrollmentConfig({
|
|
736
|
+
environment: { ...runtime.env },
|
|
737
|
+
envFile: location.configFile,
|
|
738
|
+
root: location.home,
|
|
739
|
+
});
|
|
740
|
+
if (subcommand === 'login') {
|
|
741
|
+
return await runtime.ilinkLogin({
|
|
742
|
+
config: enrollmentConfig,
|
|
743
|
+
packageRoot: runtime.packageRoot,
|
|
744
|
+
stdout: runtime.stdout,
|
|
745
|
+
stdoutIsTTY: runtime.stdoutIsTTY,
|
|
746
|
+
stdoutColumns: runtime.stdoutColumns,
|
|
747
|
+
...(qrOutputPath ? { qrOutputPath } : {}),
|
|
748
|
+
signal,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
const commandResult = await runtime.ilinkAccount({
|
|
752
|
+
command: subcommand,
|
|
753
|
+
...(parsed.values.account ? { selector: parsed.values.account } : {}),
|
|
754
|
+
confirmed: Boolean(parsed.values.yes),
|
|
755
|
+
config: enrollmentConfig,
|
|
756
|
+
packageRoot: runtime.packageRoot,
|
|
757
|
+
signal,
|
|
758
|
+
stdout: runtime.stdout,
|
|
759
|
+
});
|
|
760
|
+
if (subcommand !== 'start' || !commandResult.startForeground)
|
|
761
|
+
return 0;
|
|
762
|
+
return await runtime.ilinkStart({
|
|
763
|
+
config: loadIlinkRuntimeConfig({
|
|
764
|
+
environment: { ...runtime.env },
|
|
765
|
+
envFile: location.configFile,
|
|
766
|
+
root: location.home,
|
|
767
|
+
}),
|
|
768
|
+
signal,
|
|
769
|
+
stdout: runtime.stdout,
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
}
|
|
532
773
|
if (command === 'setup')
|
|
533
774
|
return setup(location, runtime);
|
|
534
775
|
if (command === 'start')
|