@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/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
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { openIlinkOperatorControl, } from './cli-login.js';
|
|
3
|
+
function choices(accounts, runtimeActive) {
|
|
4
|
+
return accounts.map((account, index) => ` ${index + 1}. ${JSON.stringify(account.providerAccountId)} ` +
|
|
5
|
+
`${account.accountKey} [` +
|
|
6
|
+
`${runtimeActive && account.runtimeEnabled ? 'running' : 'stopped'}]`).join('\n');
|
|
7
|
+
}
|
|
8
|
+
function selectAccount(accounts, selector, runtimeActive) {
|
|
9
|
+
if (accounts.length === 0) {
|
|
10
|
+
throw new Error('No iLink account is enrolled; run "kintio ilink login" first');
|
|
11
|
+
}
|
|
12
|
+
if (!selector) {
|
|
13
|
+
if (accounts.length === 1)
|
|
14
|
+
return accounts[0];
|
|
15
|
+
throw new Error(`Multiple iLink accounts are enrolled; use --account with one choice:\n` +
|
|
16
|
+
choices(accounts, runtimeActive));
|
|
17
|
+
}
|
|
18
|
+
const matches = accounts.filter((account) => account.accountKey === selector || account.providerAccountId === selector);
|
|
19
|
+
if (matches.length !== 1) {
|
|
20
|
+
throw new Error(`Unknown or ambiguous iLink account ${JSON.stringify(selector)}:\n` +
|
|
21
|
+
choices(accounts, runtimeActive));
|
|
22
|
+
}
|
|
23
|
+
return matches[0];
|
|
24
|
+
}
|
|
25
|
+
export async function runIlinkAccountCommand({ command, selector, confirmed = false, config, packageRoot, signal, stdout, openControl, }) {
|
|
26
|
+
if (!openControl && !fs.existsSync(config.state.databaseFile)) {
|
|
27
|
+
if (command === 'list') {
|
|
28
|
+
stdout('No iLink accounts enrolled.\n');
|
|
29
|
+
return { startForeground: false, runningCount: 0 };
|
|
30
|
+
}
|
|
31
|
+
throw new Error('No iLink account is enrolled; run "kintio ilink login" first');
|
|
32
|
+
}
|
|
33
|
+
const control = await (openControl?.() ||
|
|
34
|
+
openIlinkOperatorControl(config, packageRoot, signal));
|
|
35
|
+
try {
|
|
36
|
+
const accounts = await control.listAccounts();
|
|
37
|
+
const runtimeActive = control.mode === 'runtime';
|
|
38
|
+
if (command === 'list') {
|
|
39
|
+
stdout(accounts.length
|
|
40
|
+
? `${choices(accounts, runtimeActive)}\n`
|
|
41
|
+
: 'No iLink accounts enrolled.\n');
|
|
42
|
+
return {
|
|
43
|
+
startForeground: false,
|
|
44
|
+
runningCount: accounts.filter((account) => account.runtimeEnabled).length,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const account = selectAccount(accounts, selector, runtimeActive);
|
|
48
|
+
if (command === 'delete' && !confirmed) {
|
|
49
|
+
throw new Error(`Deleting ${JSON.stringify(account.providerAccountId)} permanently removes the account, ` +
|
|
50
|
+
'credentials, conversations, messages, media, send records, and audit records; ' +
|
|
51
|
+
'repeat with --yes');
|
|
52
|
+
}
|
|
53
|
+
const result = command === 'delete'
|
|
54
|
+
? await control.deleteAccount(account.accountKey)
|
|
55
|
+
: await control.setAccountRuntime(account.accountKey, command === 'start');
|
|
56
|
+
stdout(`${command === 'delete' ? 'Deleted' : command === 'start' ? 'Started' : 'Stopped'} ` +
|
|
57
|
+
`${JSON.stringify(account.providerAccountId)}.\n`);
|
|
58
|
+
return {
|
|
59
|
+
startForeground: command === 'start' && control.mode === 'standalone',
|
|
60
|
+
runningCount: result.runningCount,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await control.close();
|
|
65
|
+
}
|
|
66
|
+
}
|