@zhizai/cli 0.0.6 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/bin/zhizai.js +22 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +247 -26
- package/skills/zhizai-auth/SKILL.md +5 -1
package/README.md
CHANGED
|
@@ -82,12 +82,17 @@ zhizai note get <id> --field summary
|
|
|
82
82
|
|
|
83
83
|
### 4. 接入本机 AI
|
|
84
84
|
|
|
85
|
+
CLI 首次安装请用 npm(见上文「安装」)。`setup` 负责同步 Skills 与授权,**不会**在 CLI 已可用时重复 npm 重装。
|
|
86
|
+
|
|
85
87
|
```bash
|
|
86
88
|
# 预览将执行的操作
|
|
87
89
|
zhizai setup --dry-run -o json
|
|
88
90
|
|
|
89
|
-
#
|
|
91
|
+
# 同步 Skill 并引导授权(CLI 已就绪时跳过重装)
|
|
90
92
|
zhizai setup
|
|
93
|
+
|
|
94
|
+
# 升级或修复 CLI 二进制(显式触发 npm 重装)
|
|
95
|
+
zhizai setup --force-cli-install
|
|
91
96
|
```
|
|
92
97
|
|
|
93
98
|
`setup` 会把原子 Skill 安装到 Cursor、Claude Code、Codex 等本机 AI 环境,并引导完成授权。
|
|
@@ -116,7 +121,7 @@ zhizai setup
|
|
|
116
121
|
| `zhizai knowledge list [--received]` | 笔记集列表(我创建的 / 收到的) |
|
|
117
122
|
| `zhizai knowledge get <id>` | 笔记集详情 |
|
|
118
123
|
| `zhizai knowledge notes <id>` | 笔记集内笔记 |
|
|
119
|
-
| `zhizai setup [--dry-run]` |
|
|
124
|
+
| `zhizai setup [--dry-run] [--force-cli-install]` | 同步原子 Skill 并引导授权;CLI 已就绪时跳过重装 |
|
|
120
125
|
|
|
121
126
|
### 规划中
|
|
122
127
|
|
package/bin/zhizai.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
6
|
const { spawn } = require('child_process');
|
|
7
|
+
const fs = require('fs');
|
|
7
8
|
const path = require('path');
|
|
8
9
|
const os = require('os');
|
|
9
10
|
|
|
@@ -11,9 +12,29 @@ const platform = os.platform();
|
|
|
11
12
|
const binaryName = platform === 'win32' ? 'zhizai.exe' : 'zhizai';
|
|
12
13
|
const binaryPath = path.join(__dirname, binaryName);
|
|
13
14
|
|
|
15
|
+
function failMissing() {
|
|
16
|
+
console.error('无法启动智在记录:CLI 可执行文件缺失。');
|
|
17
|
+
console.error('可能原因:安装中断或文件被移除。');
|
|
18
|
+
console.error('请重新执行: npm install -g @zhizai/cli@latest');
|
|
19
|
+
console.error('错误码:CLI_BINARY_MISSING');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(binaryPath)) {
|
|
24
|
+
failMissing();
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
15
28
|
stdio: 'inherit',
|
|
16
|
-
windowsHide: true
|
|
29
|
+
windowsHide: true,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
child.on('error', (err) => {
|
|
33
|
+
if (err && err.code === 'ENOENT') {
|
|
34
|
+
failMissing();
|
|
35
|
+
}
|
|
36
|
+
console.error('无法启动智在记录:', err.message);
|
|
37
|
+
process.exit(1);
|
|
17
38
|
});
|
|
18
39
|
|
|
19
40
|
child.on('exit', (code, signal) => {
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
|
+
const http = require('http');
|
|
9
10
|
const https = require('https');
|
|
10
11
|
const crypto = require('crypto');
|
|
11
12
|
const { spawnSync } = require('child_process');
|
|
@@ -14,6 +15,34 @@ const pkg = require('../package.json');
|
|
|
14
15
|
const VERSION = pkg.version;
|
|
15
16
|
const REPO = 'BoteAI/zhizai-cli';
|
|
16
17
|
|
|
18
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 15000;
|
|
19
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 30000;
|
|
20
|
+
const DEFAULT_TOTAL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
21
|
+
const DEFAULT_RETRIES = 3;
|
|
22
|
+
const RETRY_BACKOFF_MS = [500, 1000, 2000];
|
|
23
|
+
const NON_RETRYABLE_STATUS = new Set([401, 403, 404]);
|
|
24
|
+
|
|
25
|
+
function sleep(ms) {
|
|
26
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getProtocolModule(urlString) {
|
|
30
|
+
const protocol = new URL(urlString).protocol;
|
|
31
|
+
if (protocol === 'http:') return http;
|
|
32
|
+
if (protocol === 'https:') return https;
|
|
33
|
+
throw new Error(`Unsupported protocol: ${protocol}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function versionMatches(out, expected) {
|
|
37
|
+
const m = String(out).trim().match(/^zhizai version\s+v?(\S+)/i);
|
|
38
|
+
if (!m) return false;
|
|
39
|
+
return m[1] === String(expected).replace(/^v/, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseVersionFromOutput(out, expectedVersion) {
|
|
43
|
+
return versionMatches(out, expectedVersion);
|
|
44
|
+
}
|
|
45
|
+
|
|
17
46
|
function getPlatform() {
|
|
18
47
|
const platform = os.platform();
|
|
19
48
|
const arch = os.arch();
|
|
@@ -47,6 +76,209 @@ function getWindowsExtractArgs(archivePath, destinationPath) {
|
|
|
47
76
|
];
|
|
48
77
|
}
|
|
49
78
|
|
|
79
|
+
function unlinkQuiet(filePath) {
|
|
80
|
+
try { fs.unlinkSync(filePath); } catch (_) {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function reportProgress(downloaded, contentLength, onProgress) {
|
|
84
|
+
if (contentLength != null) {
|
|
85
|
+
console.error(`Downloaded ${downloaded}/${contentLength} bytes`);
|
|
86
|
+
} else {
|
|
87
|
+
console.error(`Downloaded ${downloaded} bytes`);
|
|
88
|
+
}
|
|
89
|
+
if (onProgress) onProgress({ downloaded, contentLength });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isNonRetryableError(err) {
|
|
93
|
+
const statusCode = err && err.statusCode;
|
|
94
|
+
if (statusCode != null && NON_RETRYABLE_STATUS.has(statusCode)) return true;
|
|
95
|
+
const message = String((err && err.message) || err);
|
|
96
|
+
return /HTTP (401|403|404)/.test(message);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function downloadOnce(url, destination, options, redirects = 0) {
|
|
100
|
+
if (redirects > 5) return Promise.reject(new Error('Too many redirects'));
|
|
101
|
+
|
|
102
|
+
const {
|
|
103
|
+
connectTimeoutMs,
|
|
104
|
+
idleTimeoutMs,
|
|
105
|
+
totalTimeoutMs,
|
|
106
|
+
onProgress,
|
|
107
|
+
} = options;
|
|
108
|
+
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
let request = null;
|
|
111
|
+
let response = null;
|
|
112
|
+
let output = null;
|
|
113
|
+
let connected = false;
|
|
114
|
+
let downloaded = 0;
|
|
115
|
+
let contentLength = null;
|
|
116
|
+
let lastProgressTime = 0;
|
|
117
|
+
let idleTimer = null;
|
|
118
|
+
let totalTimer = null;
|
|
119
|
+
let connectTimer = null;
|
|
120
|
+
let settled = false;
|
|
121
|
+
|
|
122
|
+
function finish(err, value) {
|
|
123
|
+
if (settled) return;
|
|
124
|
+
settled = true;
|
|
125
|
+
clearTimeout(idleTimer);
|
|
126
|
+
clearTimeout(totalTimer);
|
|
127
|
+
clearTimeout(connectTimer);
|
|
128
|
+
if (err) reject(err);
|
|
129
|
+
else resolve(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function cleanupPartial() {
|
|
133
|
+
unlinkQuiet(destination);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function destroyStreams() {
|
|
137
|
+
if (response) {
|
|
138
|
+
try { response.destroy(); } catch (_) {}
|
|
139
|
+
}
|
|
140
|
+
if (request) {
|
|
141
|
+
try { request.destroy(); } catch (_) {}
|
|
142
|
+
}
|
|
143
|
+
if (output) {
|
|
144
|
+
try { output.destroy(); } catch (_) {}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function failTimeout(stage) {
|
|
149
|
+
destroyStreams();
|
|
150
|
+
cleanupPartial();
|
|
151
|
+
finish(new Error(`${stage} timeout`));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function resetIdleTimer() {
|
|
155
|
+
clearTimeout(idleTimer);
|
|
156
|
+
idleTimer = setTimeout(() => failTimeout('Idle'), idleTimeoutMs);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function maybeReportProgress(force = false) {
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
if (!force && now - lastProgressTime < 500) return;
|
|
162
|
+
lastProgressTime = now;
|
|
163
|
+
reportProgress(downloaded, contentLength, onProgress);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
totalTimer = setTimeout(() => failTimeout('Total'), totalTimeoutMs);
|
|
167
|
+
|
|
168
|
+
const protocol = getProtocolModule(url);
|
|
169
|
+
request = protocol.get(url, { headers: { 'User-Agent': '@zhizai/cli installer' } }, res => {
|
|
170
|
+
connected = true;
|
|
171
|
+
clearTimeout(connectTimer);
|
|
172
|
+
response = res;
|
|
173
|
+
|
|
174
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
175
|
+
res.resume();
|
|
176
|
+
destroyStreams();
|
|
177
|
+
clearTimeout(idleTimer);
|
|
178
|
+
clearTimeout(totalTimer);
|
|
179
|
+
clearTimeout(connectTimer);
|
|
180
|
+
settled = true;
|
|
181
|
+
downloadOnce(new URL(res.headers.location, url).toString(), destination, options, redirects + 1)
|
|
182
|
+
.then(resolve, reject);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (res.statusCode !== 200) {
|
|
187
|
+
res.resume();
|
|
188
|
+
destroyStreams();
|
|
189
|
+
cleanupPartial();
|
|
190
|
+
const err = new Error(`HTTP ${res.statusCode}: ${url}`);
|
|
191
|
+
err.statusCode = res.statusCode;
|
|
192
|
+
finish(err);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const lengthHeader = res.headers['content-length'];
|
|
197
|
+
if (lengthHeader != null && lengthHeader !== '') {
|
|
198
|
+
const parsed = Number(lengthHeader);
|
|
199
|
+
if (!Number.isNaN(parsed)) contentLength = parsed;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
output = fs.createWriteStream(destination, { mode: 0o600 });
|
|
203
|
+
resetIdleTimer();
|
|
204
|
+
|
|
205
|
+
res.on('data', chunk => {
|
|
206
|
+
downloaded += chunk.length;
|
|
207
|
+
resetIdleTimer();
|
|
208
|
+
maybeReportProgress();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
res.pipe(output);
|
|
212
|
+
|
|
213
|
+
output.on('finish', () => {
|
|
214
|
+
output.close(() => {
|
|
215
|
+
maybeReportProgress(true);
|
|
216
|
+
finish(null, undefined);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
output.on('error', err => {
|
|
221
|
+
destroyStreams();
|
|
222
|
+
cleanupPartial();
|
|
223
|
+
finish(err);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
res.on('error', err => {
|
|
227
|
+
destroyStreams();
|
|
228
|
+
cleanupPartial();
|
|
229
|
+
finish(err);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
connectTimer = setTimeout(() => {
|
|
234
|
+
if (!connected) failTimeout('Connect');
|
|
235
|
+
}, connectTimeoutMs);
|
|
236
|
+
|
|
237
|
+
request.on('socket', socket => {
|
|
238
|
+
socket.once('connect', () => {
|
|
239
|
+
connected = true;
|
|
240
|
+
clearTimeout(connectTimer);
|
|
241
|
+
resetIdleTimer();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
request.on('error', err => {
|
|
246
|
+
destroyStreams();
|
|
247
|
+
cleanupPartial();
|
|
248
|
+
finish(err);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function download(url, destination, options = {}, redirects = 0) {
|
|
254
|
+
const opts = {
|
|
255
|
+
connectTimeoutMs: DEFAULT_CONNECT_TIMEOUT_MS,
|
|
256
|
+
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
257
|
+
totalTimeoutMs: DEFAULT_TOTAL_TIMEOUT_MS,
|
|
258
|
+
retries: DEFAULT_RETRIES,
|
|
259
|
+
onProgress: null,
|
|
260
|
+
...options,
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
if (redirects > 5) throw new Error('Too many redirects');
|
|
264
|
+
|
|
265
|
+
const maxRetries = opts.retries;
|
|
266
|
+
let lastError;
|
|
267
|
+
|
|
268
|
+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
269
|
+
try {
|
|
270
|
+
return await downloadOnce(url, destination, opts, redirects);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
lastError = err;
|
|
273
|
+
if (isNonRetryableError(err) || attempt >= maxRetries) throw err;
|
|
274
|
+
const delay = RETRY_BACKOFF_MS[Math.min(attempt, RETRY_BACKOFF_MS.length - 1)];
|
|
275
|
+
await sleep(delay);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
throw lastError;
|
|
280
|
+
}
|
|
281
|
+
|
|
50
282
|
async function installArchive({ platform, binDir, binaryName, binaryPath, url, tmpFile }) {
|
|
51
283
|
try {
|
|
52
284
|
await download(url, tmpFile);
|
|
@@ -60,13 +292,12 @@ async function installArchive({ platform, binDir, binaryName, binaryPath, url, t
|
|
|
60
292
|
if (!fs.existsSync(binaryPath)) {
|
|
61
293
|
throw new Error(`Binary missing after extract: ${binaryPath}`);
|
|
62
294
|
}
|
|
63
|
-
// chmod is meaningful on Unix; Windows may not need it.
|
|
64
295
|
if (platform.platform !== 'windows') {
|
|
65
296
|
fs.chmodSync(binaryPath, 0o755);
|
|
66
297
|
}
|
|
67
298
|
console.log(`zhizai installed at ${binaryPath}`);
|
|
68
299
|
} finally {
|
|
69
|
-
|
|
300
|
+
unlinkQuiet(tmpFile);
|
|
70
301
|
}
|
|
71
302
|
}
|
|
72
303
|
|
|
@@ -76,7 +307,6 @@ async function main() {
|
|
|
76
307
|
const binaryName = getBinaryName(platform);
|
|
77
308
|
const binaryPath = path.join(binDir, binaryName);
|
|
78
309
|
const url = getDownloadURL(platform);
|
|
79
|
-
// Expand-Archive on Windows requires a .zip extension; tar.gz similarly.
|
|
80
310
|
const archiveExt = platform.platform === 'windows' ? '.zip' : '.tar.gz';
|
|
81
311
|
const tmpFile = path.join(os.tmpdir(), `zhizai-download-${Date.now()}${archiveExt}`);
|
|
82
312
|
|
|
@@ -84,7 +314,7 @@ async function main() {
|
|
|
84
314
|
try {
|
|
85
315
|
const result = spawnSync(binaryPath, ['version'], { encoding: 'utf8' });
|
|
86
316
|
const out = (result.stdout || '').trim();
|
|
87
|
-
if (result.status === 0 && out
|
|
317
|
+
if (result.status === 0 && versionMatches(out, VERSION)) {
|
|
88
318
|
console.log(`zhizai v${VERSION} already installed, skipping download.`);
|
|
89
319
|
return;
|
|
90
320
|
}
|
|
@@ -104,27 +334,6 @@ function run(command, args) {
|
|
|
104
334
|
if (result.status !== 0) throw new Error(`${command} exited with status ${result.status}`);
|
|
105
335
|
}
|
|
106
336
|
|
|
107
|
-
function download(url, destination, redirects = 0) {
|
|
108
|
-
if (redirects > 5) return Promise.reject(new Error('Too many redirects'));
|
|
109
|
-
return new Promise((resolve, reject) => {
|
|
110
|
-
const request = https.get(url, { headers: { 'User-Agent': '@zhizai/cli installer' } }, response => {
|
|
111
|
-
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
112
|
-
response.resume();
|
|
113
|
-
return resolve(download(new URL(response.headers.location, url).toString(), destination, redirects + 1));
|
|
114
|
-
}
|
|
115
|
-
if (response.statusCode !== 200) {
|
|
116
|
-
response.resume();
|
|
117
|
-
return reject(new Error(`HTTP ${response.statusCode}: ${url}`));
|
|
118
|
-
}
|
|
119
|
-
const output = fs.createWriteStream(destination, { mode: 0o600 });
|
|
120
|
-
response.pipe(output);
|
|
121
|
-
output.on('finish', () => output.close(resolve));
|
|
122
|
-
output.on('error', reject);
|
|
123
|
-
});
|
|
124
|
-
request.on('error', reject);
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
337
|
async function verifyChecksum(assetURL, assetName, archivePath) {
|
|
129
338
|
const checksumPath = `${archivePath}.checksums`;
|
|
130
339
|
try {
|
|
@@ -139,10 +348,22 @@ async function verifyChecksum(assetURL, assetName, archivePath) {
|
|
|
139
348
|
const actual = crypto.createHash('sha256').update(fs.readFileSync(archivePath)).digest('hex');
|
|
140
349
|
if (actual !== expected) throw new Error(`Checksum mismatch for ${assetName}`);
|
|
141
350
|
} finally {
|
|
142
|
-
|
|
351
|
+
unlinkQuiet(checksumPath);
|
|
143
352
|
}
|
|
144
353
|
}
|
|
145
354
|
|
|
355
|
+
module.exports = {
|
|
356
|
+
download,
|
|
357
|
+
downloadOnce,
|
|
358
|
+
versionMatches,
|
|
359
|
+
parseVersionFromOutput,
|
|
360
|
+
getPlatform,
|
|
361
|
+
getBinaryName,
|
|
362
|
+
getDownloadURL,
|
|
363
|
+
verifyChecksum,
|
|
364
|
+
installArchive,
|
|
365
|
+
};
|
|
366
|
+
|
|
146
367
|
if (require.main === module) {
|
|
147
368
|
main().catch(err => {
|
|
148
369
|
console.error('Failed to install zhizai:', err.message);
|
|
@@ -8,16 +8,20 @@ description: 安装和连接智在记录,完成网页授权登录、环境诊
|
|
|
8
8
|
|
|
9
9
|
通过官方 `zhizai` CLI 完成真实操作。机器调用优先使用 `-o json`。
|
|
10
10
|
|
|
11
|
+
**安装与升级:** 首次安装 CLI 用 `npm install -g @zhizai/cli@latest`。`zhizai setup` 默认同步 Skills 并引导授权;CLI 已可用时**不会**重复 npm 重装。需要升级或修复二进制时用 `zhizai setup --force-cli-install`。
|
|
12
|
+
|
|
11
13
|
## 路由
|
|
12
14
|
|
|
13
15
|
| 意图 | 命令 |
|
|
14
16
|
|---|---|
|
|
17
|
+
| 首次安装 CLI | `npm install -g @zhizai/cli@latest` |
|
|
15
18
|
| 登录 | `zhizai auth login` |
|
|
16
19
|
| 无头/连接器登录 | `zhizai auth login --no-open` |
|
|
17
20
|
| 查看状态 | `zhizai auth status` |
|
|
18
21
|
| 退出 | `zhizai auth logout` |
|
|
19
22
|
| 诊断 | `zhizai doctor -o json` |
|
|
20
23
|
| 能力契约 | `zhizai capabilities -o json` |
|
|
21
|
-
|
|
|
24
|
+
| 同步 Skill / 授权 | `zhizai setup` |
|
|
25
|
+
| 升级或修复 CLI | `zhizai setup --force-cli-install` |
|
|
22
26
|
|
|
23
27
|
默认使用网页设备授权。`--no-open` 时不打开浏览器,打印 `[verify_url]` / `[device_code]` / `[expires_in]` / `[interval]` 供宿主展示,后台继续轮询。仅当网页授权失败时,再提示用户使用 `zhizai auth login --api-key <key>`。
|