@kin-tio/cli 0.6.2 → 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 +26 -0
- package/README.md +52 -15
- package/README.zh-CN.md +33 -9
- 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 +1 -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,32 @@ 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
|
+
|
|
7
33
|
## 0.6.2
|
|
8
34
|
|
|
9
35
|
- Added a custom Kintio wordmark and its circular-safe TIO avatar to the English
|
package/README.md
CHANGED
|
@@ -67,33 +67,67 @@ Prerequisites:
|
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
69
|
npm install --global @kin-tio/cli
|
|
70
|
-
kintio setup
|
|
71
70
|
codex login status
|
|
72
71
|
```
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
skill, and writes the channel configuration template. POSIX systems use mode `0600`;
|
|
76
|
-
Windows keeps the instance inside the current user's profile. No adapter is enabled
|
|
77
|
-
by default. Follow the [setup guide](https://github.com/Gkxie/kintio/blob/master/docs/setup.md)
|
|
78
|
-
and edit `~/.kintio/.env` to configure one adapter:
|
|
73
|
+
For an iLink-only instance, no setup file or public HTTP listener is required:
|
|
79
74
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
currently starts from an authorized WeChat KF conversation.
|
|
75
|
+
```bash
|
|
76
|
+
kintio ilink login
|
|
77
|
+
kintio ilink start
|
|
78
|
+
```
|
|
85
79
|
|
|
86
|
-
|
|
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:
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
|
+
kintio setup
|
|
89
90
|
kintio start
|
|
90
91
|
kintio status
|
|
91
92
|
kintio logs --lines 100
|
|
92
93
|
```
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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.
|
|
97
131
|
Existing source-based deployments can keep their current state after the one-time
|
|
98
132
|
process-manager migration described in the setup guide.
|
|
99
133
|
|
|
@@ -111,6 +145,9 @@ Source builds and contributor setup are documented in
|
|
|
111
145
|
Hono route; every action still requires a short-lived conversation capability.
|
|
112
146
|
- Project-level Agent capability restrictions are not an operating-system sandbox. Use a
|
|
113
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.
|
|
114
151
|
- A provider accepting an outbound request does not prove that a client displayed it;
|
|
115
152
|
uncertain outcomes remain explicit to avoid duplicate delivery.
|
|
116
153
|
|
package/README.zh-CN.md
CHANGED
|
@@ -43,25 +43,49 @@
|
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
npm install --global @kin-tio/cli
|
|
46
|
-
kintio setup
|
|
47
46
|
codex login status
|
|
48
47
|
```
|
|
49
48
|
|
|
50
|
-
`
|
|
51
|
-
模板 `.env`。macOS/Linux 使用 `0600`,Windows 则限定在当前用户目录的 ACL
|
|
52
|
-
边界内。默认不启用任何适配器;请按英文
|
|
53
|
-
[部署指南](https://github.com/Gkxie/kintio/blob/master/docs/setup.md)配置 WeChat KF API,或为已有 Weixin iLink 绑定设置
|
|
54
|
-
`ILINK_ENABLED=true`。
|
|
49
|
+
iLink 可以完全独立使用,不需要 `setup`、`.env` 或公网 HTTP:
|
|
55
50
|
|
|
56
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
|
|
57
65
|
kintio start
|
|
58
66
|
kintio status
|
|
59
67
|
kintio logs --lines 100
|
|
60
68
|
```
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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`。
|
|
65
89
|
源码构建与贡献者开发环境见英文
|
|
66
90
|
[贡献指南](https://github.com/Gkxie/kintio/blob/master/CONTRIBUTING.md)。
|
|
67
91
|
|
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')
|
package/dist/src/config.js
CHANGED
|
@@ -124,6 +124,40 @@ function parseBoundedText(value, fallback, name, maxBytes) {
|
|
|
124
124
|
}
|
|
125
125
|
return parsed;
|
|
126
126
|
}
|
|
127
|
+
function createIlinkEnrollmentConfig(environment = process.env, root, platform = process.platform) {
|
|
128
|
+
environment = copyEnvironment(environment);
|
|
129
|
+
const home = path.resolve(root || resolveInstanceRoot(environment));
|
|
130
|
+
const storageKey = String(environment.ILINK_STORAGE_KEY || '').trim();
|
|
131
|
+
if (storageKey && !/^[A-Za-z0-9_-]{43}$/u.test(storageKey)) {
|
|
132
|
+
throw new Error('ILINK_STORAGE_KEY must be a canonical 32-byte base64url value');
|
|
133
|
+
}
|
|
134
|
+
const state = resolveStateFiles(environment, home);
|
|
135
|
+
const storageKeyFile = path.resolve(home, environment.ILINK_STORAGE_KEY_FILE ||
|
|
136
|
+
path.join(path.dirname(state.databaseFile), 'ilink-storage.key'));
|
|
137
|
+
if (platform === 'win32') {
|
|
138
|
+
for (const [name, filePath] of [
|
|
139
|
+
['KINTIO_DB_FILE', state.databaseFile],
|
|
140
|
+
['Kintio state lock', state.lockFile],
|
|
141
|
+
['ILINK_STORAGE_KEY_FILE', storageKeyFile],
|
|
142
|
+
]) {
|
|
143
|
+
if (!isPathInside(home, filePath)) {
|
|
144
|
+
throw new Error(`${name} must stay inside KINTIO_HOME on Windows`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return Object.freeze({
|
|
149
|
+
home,
|
|
150
|
+
state,
|
|
151
|
+
ilink: Object.freeze({
|
|
152
|
+
storageKey,
|
|
153
|
+
storageKeyFile,
|
|
154
|
+
baseUrl: environment.ILINK_BASE_URL || 'https://ilinkai.weixin.qq.com/',
|
|
155
|
+
apiTimeoutMs: parsePositiveInteger(environment.ILINK_API_TIMEOUT_MS, 15_000, 'ILINK_API_TIMEOUT_MS', 120_000),
|
|
156
|
+
longPollTimeoutMs: parsePositiveInteger(environment.ILINK_LONG_POLL_TIMEOUT_MS, 35_000, 'ILINK_LONG_POLL_TIMEOUT_MS', 120_000),
|
|
157
|
+
maxAccounts: parsePositiveInteger(environment.ILINK_MAX_ACCOUNTS, 20, 'ILINK_MAX_ACCOUNTS', 1_000),
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
127
161
|
export function createConfig(environment = process.env, root, platform = process.platform) {
|
|
128
162
|
environment = copyEnvironment(environment);
|
|
129
163
|
const instanceRoot = path.resolve(root || resolveInstanceRoot(environment));
|
|
@@ -150,22 +184,14 @@ export function createConfig(environment = process.env, root, platform = process
|
|
|
150
184
|
}
|
|
151
185
|
const ilinkEnabled = parseBoolean(environment.ILINK_ENABLED, false);
|
|
152
186
|
const codexEnabled = parseBoolean(environment.CODEX_ENABLED, apiEnabled || ilinkEnabled);
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
throw new Error('ILINK_STORAGE_KEY must be a canonical 32-byte base64url value');
|
|
156
|
-
}
|
|
157
|
-
const { databaseFile, lockFile } = resolveStateFiles(environment, instanceRoot);
|
|
187
|
+
const enrollment = createIlinkEnrollmentConfig(environment, instanceRoot, platform);
|
|
188
|
+
const { databaseFile, lockFile } = enrollment.state;
|
|
158
189
|
const codexWorkingDirectory = path.resolve(instanceRoot, environment.CODEX_WORKING_DIRECTORY ||
|
|
159
190
|
'codex-workspace');
|
|
160
|
-
const ilinkStorageKeyFile = path.resolve(instanceRoot, environment.ILINK_STORAGE_KEY_FILE ||
|
|
161
|
-
path.join(path.dirname(databaseFile), 'ilink-storage.key'));
|
|
162
191
|
const codexImageTempDirectory = path.resolve(instanceRoot, environment.CODEX_IMAGE_TMP_DIR ||
|
|
163
192
|
'data/codex-input');
|
|
164
193
|
if (platform === 'win32') {
|
|
165
194
|
for (const [name, filePath] of [
|
|
166
|
-
['KINTIO_DB_FILE', databaseFile],
|
|
167
|
-
['Kintio state lock', lockFile],
|
|
168
|
-
['ILINK_STORAGE_KEY_FILE', ilinkStorageKeyFile],
|
|
169
195
|
['CODEX_IMAGE_TMP_DIR', codexImageTempDirectory],
|
|
170
196
|
]) {
|
|
171
197
|
if (!isPathInside(instanceRoot, filePath)) {
|
|
@@ -201,12 +227,7 @@ export function createConfig(environment = process.env, root, platform = process
|
|
|
201
227
|
}),
|
|
202
228
|
ilink: Object.freeze({
|
|
203
229
|
enabled: ilinkEnabled,
|
|
204
|
-
|
|
205
|
-
storageKeyFile: ilinkStorageKeyFile,
|
|
206
|
-
baseUrl: environment.ILINK_BASE_URL || 'https://ilinkai.weixin.qq.com/',
|
|
207
|
-
apiTimeoutMs: parsePositiveInteger(environment.ILINK_API_TIMEOUT_MS, 15_000, 'ILINK_API_TIMEOUT_MS', 120_000),
|
|
208
|
-
longPollTimeoutMs: parsePositiveInteger(environment.ILINK_LONG_POLL_TIMEOUT_MS, 35_000, 'ILINK_LONG_POLL_TIMEOUT_MS', 120_000),
|
|
209
|
-
maxAccounts: parsePositiveInteger(environment.ILINK_MAX_ACCOUNTS, 20, 'ILINK_MAX_ACCOUNTS', 1_000),
|
|
230
|
+
...enrollment.ilink,
|
|
210
231
|
}),
|
|
211
232
|
state: Object.freeze({
|
|
212
233
|
databaseFile,
|
|
@@ -222,6 +243,10 @@ export function createConfig(environment = process.env, root, platform = process
|
|
|
222
243
|
});
|
|
223
244
|
}
|
|
224
245
|
export function loadConfig(options = {}) {
|
|
246
|
+
const loaded = loadConfigurationEnvironment(options);
|
|
247
|
+
return createConfig(loaded.environment, loaded.root);
|
|
248
|
+
}
|
|
249
|
+
function loadConfigurationEnvironment(options) {
|
|
225
250
|
const environment = copyEnvironment(options.environment || process.env);
|
|
226
251
|
const configuredEnvFile = options.envFile || environment.KINTIO_CONFIG_FILE;
|
|
227
252
|
const defaultRoot = path.join(path.resolve(options.homeDirectory || os.homedir()), '.kintio');
|
|
@@ -233,5 +258,35 @@ export function loadConfig(options = {}) {
|
|
|
233
258
|
const envFile = path.resolve(configuredEnvFile || path.join(initialRoot, '.env'));
|
|
234
259
|
loadEnvironmentFile(envFile, environment);
|
|
235
260
|
const root = path.resolve(options.root || environment.KINTIO_HOME || initialRoot);
|
|
236
|
-
return
|
|
261
|
+
return { environment, root };
|
|
262
|
+
}
|
|
263
|
+
export function loadIlinkEnrollmentConfig(options = {}) {
|
|
264
|
+
const loaded = loadConfigurationEnvironment(options);
|
|
265
|
+
return createIlinkEnrollmentConfig(loaded.environment, loaded.root);
|
|
266
|
+
}
|
|
267
|
+
export function loadIlinkRuntimeConfig(options = {}) {
|
|
268
|
+
const { environment, root } = loadConfigurationEnvironment(options);
|
|
269
|
+
const enrollment = createIlinkEnrollmentConfig(environment, root);
|
|
270
|
+
const workingDirectory = path.resolve(root, environment.CODEX_WORKING_DIRECTORY || 'codex-workspace');
|
|
271
|
+
const imageTempDirectory = path.resolve(root, environment.CODEX_IMAGE_TMP_DIR || 'data/codex-input');
|
|
272
|
+
if (process.platform === 'win32' && !isPathInside(root, imageTempDirectory)) {
|
|
273
|
+
throw new Error('CODEX_IMAGE_TMP_DIR must stay inside KINTIO_HOME on Windows');
|
|
274
|
+
}
|
|
275
|
+
const shutdownTimeoutMs = parsePositiveInteger(environment.SHUTDOWN_TIMEOUT_MS, 10_000, 'SHUTDOWN_TIMEOUT_MS', MAX_SHUTDOWN_TIMEOUT_MS);
|
|
276
|
+
if (shutdownTimeoutMs < 1_000) {
|
|
277
|
+
throw new Error('SHUTDOWN_TIMEOUT_MS must be at least 1000');
|
|
278
|
+
}
|
|
279
|
+
return Object.freeze({
|
|
280
|
+
state: Object.freeze({
|
|
281
|
+
...enrollment.state,
|
|
282
|
+
shutdownTimeoutMs,
|
|
283
|
+
}),
|
|
284
|
+
ilink: Object.freeze({ enabled: true, ...enrollment.ilink }),
|
|
285
|
+
codex: Object.freeze({
|
|
286
|
+
enabled: true,
|
|
287
|
+
imageTempDirectory,
|
|
288
|
+
workingDirectory,
|
|
289
|
+
generatedImageDirectory: path.join(workingDirectory, 'generated_images'),
|
|
290
|
+
}),
|
|
291
|
+
});
|
|
237
292
|
}
|