@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 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
- # 正式安装 Skill
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]` | 为本机 AI 安装原子 Skill 并引导授权 |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhizai/cli",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "CLI tool for 智在记录 — manage notes and knowledge from the terminal and AI agents",
5
5
  "keywords": [
6
6
  "zhizai",
@@ -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
- try { fs.unlinkSync(tmpFile); } catch (_) {}
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.includes(VERSION)) {
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
- try { fs.unlinkSync(checksumPath); } catch (_) {}
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
- | 安装 Skill | `zhizai setup` |
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>`。