@dreamlogic-ai/cli 2.0.5 → 2.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/dist/commands/install.js +20 -0
- package/dist/commands/list.js +1 -1
- package/dist/commands/setup-mcp.js +4 -1
- package/dist/commands/update.js +5 -2
- package/dist/index.js +46 -9
- package/dist/lib/api-client.d.ts +2 -0
- package/dist/lib/api-client.js +5 -1
- package/dist/lib/config.js +21 -9
- package/dist/lib/installer.js +31 -0
- package/dist/types.d.ts +2 -1
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/dist/commands/install.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as clack from "@clack/prompts";
|
|
6
6
|
import { join } from "path";
|
|
7
|
+
import { statfsSync } from "fs";
|
|
7
8
|
import { ApiClient } from "../lib/api-client.js";
|
|
8
9
|
import { getServer, getInstallDir, loadInstalled } from "../lib/config.js";
|
|
9
10
|
import { installSkill } from "../lib/installer.js";
|
|
@@ -110,6 +111,25 @@ export async function installCommand(skillIds, opts) {
|
|
|
110
111
|
return;
|
|
111
112
|
}
|
|
112
113
|
}
|
|
114
|
+
// R4-FIX #8: 安装前检查磁盘空间(可选警告)
|
|
115
|
+
try {
|
|
116
|
+
const totalSize = selectedIds.reduce((sum, id) => {
|
|
117
|
+
const s = skills.find((s) => s.id === id);
|
|
118
|
+
return sum + (s?.package_size || 0);
|
|
119
|
+
}, 0);
|
|
120
|
+
if (totalSize > 0) {
|
|
121
|
+
const installPath = getInstallDir();
|
|
122
|
+
const { mkdirSync } = await import("fs");
|
|
123
|
+
mkdirSync(installPath, { recursive: true });
|
|
124
|
+
const fsStats = statfsSync(installPath);
|
|
125
|
+
const freeSpace = fsStats.bsize * fsStats.bavail;
|
|
126
|
+
// 需要解压后约 2x 空间(压缩包 + 解压内容)
|
|
127
|
+
if (freeSpace < totalSize * 2) {
|
|
128
|
+
ui.warning(`磁盘剩余空间不足: ${ui.fileSize(freeSpace)} 可用, 预计需要 ${ui.fileSize(totalSize * 2)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch { /* best-effort: statfsSync 可能在某些平台不可用 */ }
|
|
113
133
|
// Install each
|
|
114
134
|
let successCount = 0;
|
|
115
135
|
for (const id of selectedIds) {
|
package/dist/commands/list.js
CHANGED
|
@@ -20,7 +20,7 @@ export function listCommand() {
|
|
|
20
20
|
["版本", info.version],
|
|
21
21
|
["安装时间", new Date(info.installed_at).toLocaleString()],
|
|
22
22
|
["路径", info.path],
|
|
23
|
-
["SHA256", info.sha256.slice(0, 16) + "..."],
|
|
23
|
+
["SHA256", (info.sha256 || "unknown").slice(0, 16) + "..."],
|
|
24
24
|
]);
|
|
25
25
|
if (info.previous_version) {
|
|
26
26
|
ui.line(` ${ui.dim(`上一版本: ${info.previous_version}`)}`);
|
|
@@ -36,7 +36,7 @@ function getAgents(server, key) {
|
|
|
36
36
|
{
|
|
37
37
|
name: "Claude Desktop",
|
|
38
38
|
configPath: claudeDesktopPath,
|
|
39
|
-
detect: () => existsSync(claudeDesktopPath)
|
|
39
|
+
detect: () => existsSync(claudeDesktopPath), // R4-FIX #4: 只检查配置文件本身,避免父目录误检
|
|
40
40
|
generate: () => ({
|
|
41
41
|
mcpServers: {
|
|
42
42
|
"dreamlogic-skills": {
|
|
@@ -46,6 +46,8 @@ function getAgents(server, key) {
|
|
|
46
46
|
},
|
|
47
47
|
}),
|
|
48
48
|
apply: (newSection) => {
|
|
49
|
+
// R4-FIX #5: read-then-write 存在 TOCTOU 竞态,但此为单用户 CLI 工具,
|
|
50
|
+
// 风险极低。如需更强保护可使用 flock,但当前场景不需要。
|
|
49
51
|
let existing = {};
|
|
50
52
|
if (existsSync(claudeDesktopPath)) {
|
|
51
53
|
try {
|
|
@@ -108,6 +110,7 @@ function getAgents(server, key) {
|
|
|
108
110
|
},
|
|
109
111
|
}),
|
|
110
112
|
apply: (newSection) => {
|
|
113
|
+
// R4-FIX #5: read-then-write TOCTOU — 单用户 CLI 风险极低,添加注释说明
|
|
111
114
|
const path = join(home, ".cursor", "mcp.json");
|
|
112
115
|
let existing = {};
|
|
113
116
|
if (existsSync(path)) {
|
package/dist/commands/update.js
CHANGED
|
@@ -49,11 +49,14 @@ export async function updateCommand(opts) {
|
|
|
49
49
|
const normalizeVer = (v) => v.replace(/^v/, "");
|
|
50
50
|
const localVer = normalizeVer(local.version);
|
|
51
51
|
const remoteVer = normalizeVer(remote.latest_version);
|
|
52
|
+
// R5-M03 FIX: Numeric comparison to prevent non-standard version bypass
|
|
53
|
+
const localNum = parseInt(localVer.replace(/\D/g, ""), 10);
|
|
54
|
+
const remoteNum = parseInt(remoteVer.replace(/\D/g, ""), 10);
|
|
52
55
|
if (localVer === remoteVer) {
|
|
53
56
|
ui.line(`${chalk.green("✓")} ${remote.name} ${local.version} — 已是最新`);
|
|
54
57
|
}
|
|
55
|
-
else if (
|
|
56
|
-
// R4-C02 FIX: Block version downgrade attacks
|
|
58
|
+
else if (!isNaN(localNum) && !isNaN(remoteNum) && remoteNum < localNum) {
|
|
59
|
+
// R4-C02 + R5-M03 FIX: Block version downgrade attacks (numeric comparison)
|
|
57
60
|
ui.line(`${ui.warn("⚠")} ${remote.name} — 服务器版本 ${remote.latest_version} 低于本地 ${local.version}(可能的降级攻击,已跳过)`);
|
|
58
61
|
}
|
|
59
62
|
else {
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import * as clack from "@clack/prompts";
|
|
|
10
10
|
import { CLI_VERSION, CLI_NAME, CLI_AUTHOR } from "./types.js";
|
|
11
11
|
import { ui } from "./lib/ui.js";
|
|
12
12
|
import { getApiKey } from "./lib/config.js";
|
|
13
|
+
import boxen from "boxen";
|
|
13
14
|
// Commands
|
|
14
15
|
import { loginCommand } from "./commands/login.js";
|
|
15
16
|
import { logoutCommand } from "./commands/logout.js";
|
|
@@ -29,8 +30,15 @@ program
|
|
|
29
30
|
program
|
|
30
31
|
.command("login")
|
|
31
32
|
.description("登录 — 验证 API Key")
|
|
32
|
-
|
|
33
|
-
.
|
|
33
|
+
// R4-FIX #1: API Key 通过 CLI 参数暴露风险警告
|
|
34
|
+
.option("-k, --key <key>", "API Key(⚠️ 不推荐:会暴露在进程列表和 shell 历史中,建议用交互输入或环境变量)")
|
|
35
|
+
.action((opts) => {
|
|
36
|
+
// R4-FIX #1: 清除 process.title 中的敏感参数
|
|
37
|
+
if (opts.key) {
|
|
38
|
+
process.title = "dreamlogic login";
|
|
39
|
+
}
|
|
40
|
+
return loginCommand(opts);
|
|
41
|
+
});
|
|
34
42
|
// ===== logout =====
|
|
35
43
|
program
|
|
36
44
|
.command("logout")
|
|
@@ -134,10 +142,25 @@ program.action(async () => {
|
|
|
134
142
|
// Handle errors gracefully
|
|
135
143
|
program.exitOverride();
|
|
136
144
|
async function main() {
|
|
137
|
-
//
|
|
145
|
+
// R4-FIX #9: TLS 验证禁用 — 醒目安全警告(红色框)
|
|
138
146
|
if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") {
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
const tlsWarning = [
|
|
148
|
+
"NODE_TLS_REJECT_UNAUTHORIZED=0 已检测到",
|
|
149
|
+
"TLS 证书验证已完全禁用!",
|
|
150
|
+
"",
|
|
151
|
+
"风险:所有 HTTPS 请求均不验证服务器身份,",
|
|
152
|
+
"可能遭受中间人攻击 (MITM)。",
|
|
153
|
+
"",
|
|
154
|
+
"修复:unset NODE_TLS_REJECT_UNAUTHORIZED",
|
|
155
|
+
].join("\n");
|
|
156
|
+
console.log(boxen(tlsWarning, {
|
|
157
|
+
title: "⚠️ 安全警告",
|
|
158
|
+
titleAlignment: "center",
|
|
159
|
+
padding: 1,
|
|
160
|
+
margin: { top: 1, bottom: 1, left: 2, right: 0 },
|
|
161
|
+
borderStyle: "double",
|
|
162
|
+
borderColor: "red",
|
|
163
|
+
}));
|
|
141
164
|
}
|
|
142
165
|
try {
|
|
143
166
|
await program.parseAsync(process.argv);
|
|
@@ -150,13 +173,27 @@ async function main() {
|
|
|
150
173
|
}
|
|
151
174
|
}
|
|
152
175
|
// CLI-001: clack returns isCancel symbol (handled at each prompt site)
|
|
153
|
-
//
|
|
154
|
-
if (err instanceof Error &&
|
|
176
|
+
// R5-M01 FIX: Only match exact clack cancel errors, not all messages containing "cancel"
|
|
177
|
+
if (clack.isCancel(err) || (err instanceof Error && err.message === "Cancelled")) {
|
|
155
178
|
console.log();
|
|
156
|
-
clack.cancel("
|
|
179
|
+
clack.cancel("已取消。");
|
|
157
180
|
return;
|
|
158
181
|
}
|
|
159
|
-
|
|
182
|
+
// R4-FIX #10: 错误信息不泄漏内部路径/堆栈
|
|
183
|
+
const knownErrors = ["ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "EACCES", "EPERM"];
|
|
184
|
+
const errMsg = err.message || "";
|
|
185
|
+
if (knownErrors.some(code => errMsg.includes(code))) {
|
|
186
|
+
ui.err(`操作失败: ${errMsg}`);
|
|
187
|
+
}
|
|
188
|
+
else if (errMsg.includes("SHA256") || errMsg.includes("Invalid skill ID")) {
|
|
189
|
+
ui.err(errMsg);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
ui.err("发生意外错误,请重试或联系支持。");
|
|
193
|
+
if (process.env.DREAMLOGIC_DEBUG === "1") {
|
|
194
|
+
ui.line(`调试信息: ${errMsg}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
160
197
|
process.exitCode = 1;
|
|
161
198
|
}
|
|
162
199
|
}
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export declare class ApiClient {
|
|
|
12
12
|
downloadPackage(filename: string, onProgress?: (downloaded: number, total: number) => void): Promise<{
|
|
13
13
|
buffer: Buffer;
|
|
14
14
|
sha256: string;
|
|
15
|
+
contentLength: number;
|
|
16
|
+
warnLargeDownload: boolean;
|
|
15
17
|
}>;
|
|
16
18
|
/** GET /health — check server connectivity (R2-09: validated) */
|
|
17
19
|
health(): Promise<{
|
package/dist/lib/api-client.js
CHANGED
|
@@ -11,6 +11,8 @@ import { CLI_VERSION } from "../types.js";
|
|
|
11
11
|
const API_TIMEOUT = 30_000; // 30s for API calls
|
|
12
12
|
const DOWNLOAD_TIMEOUT = 300_000; // 5min for downloads
|
|
13
13
|
const MAX_DOWNLOAD_SIZE = 200 * 1024 * 1024; // 200MB
|
|
14
|
+
// R4-FIX #3: 下载预警阈值
|
|
15
|
+
const DOWNLOAD_WARN_SIZE = 50 * 1024 * 1024; // 50MB
|
|
14
16
|
export class ApiClient {
|
|
15
17
|
baseUrl;
|
|
16
18
|
apiKey;
|
|
@@ -81,6 +83,8 @@ export class ApiClient {
|
|
|
81
83
|
if (!Number.isNaN(contentLength) && contentLength > 0 && contentLength > MAX_DOWNLOAD_SIZE) {
|
|
82
84
|
throw new ApiError(`Package too large: ${contentLength} bytes (max ${MAX_DOWNLOAD_SIZE})`, 413);
|
|
83
85
|
}
|
|
86
|
+
// R4-FIX #3: 大包下载预警 — 通过回调通知调用者
|
|
87
|
+
const warnLargeDownload = !Number.isNaN(contentLength) && contentLength > DOWNLOAD_WARN_SIZE;
|
|
84
88
|
const reader = res.body?.getReader();
|
|
85
89
|
if (!reader)
|
|
86
90
|
throw new ApiError("No response body", 500);
|
|
@@ -103,7 +107,7 @@ export class ApiClient {
|
|
|
103
107
|
}
|
|
104
108
|
const buffer = Buffer.concat(chunks);
|
|
105
109
|
const sha256 = createHash("sha256").update(buffer).digest("hex");
|
|
106
|
-
return { buffer, sha256 };
|
|
110
|
+
return { buffer, sha256, contentLength, warnLargeDownload };
|
|
107
111
|
}
|
|
108
112
|
/** GET /health — check server connectivity (R2-09: validated) */
|
|
109
113
|
async health() {
|
package/dist/lib/config.js
CHANGED
|
@@ -4,8 +4,19 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, statSync } from "fs";
|
|
6
6
|
import { join, resolve } from "path";
|
|
7
|
-
import { homedir } from "os";
|
|
7
|
+
import { homedir, platform } from "os";
|
|
8
8
|
import { CONFIG_DIR_NAME, DEFAULT_SERVER, DEFAULT_INSTALL_DIR_NAME, } from "../types.js";
|
|
9
|
+
// R4-FIX #11: Windows 不支持 Unix 文件权限,跳过 chmod
|
|
10
|
+
const isWin = platform() === "win32";
|
|
11
|
+
/** Platform-safe chmod: 只在 Unix 系统上执行 */
|
|
12
|
+
function safeChmod(path, mode) {
|
|
13
|
+
if (isWin)
|
|
14
|
+
return; // R4-FIX #11: Windows 下跳过
|
|
15
|
+
try {
|
|
16
|
+
chmodSync(path, mode);
|
|
17
|
+
}
|
|
18
|
+
catch { /* best-effort */ }
|
|
19
|
+
}
|
|
9
20
|
/** CFG-01 FIX: Recursively strip prototype pollution keys from parsed JSON */
|
|
10
21
|
function sanitize(obj) {
|
|
11
22
|
if (typeof obj !== "object" || obj === null)
|
|
@@ -61,10 +72,7 @@ export function saveConfig(config) {
|
|
|
61
72
|
ensureConfigDir();
|
|
62
73
|
const path = getConfigPath();
|
|
63
74
|
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
|
64
|
-
|
|
65
|
-
chmodSync(path, 0o600);
|
|
66
|
-
}
|
|
67
|
-
catch { /* Windows fallback */ }
|
|
75
|
+
safeChmod(path, 0o600);
|
|
68
76
|
}
|
|
69
77
|
/** R1-06: Validate key format from all sources (env, config file) */
|
|
70
78
|
export function getApiKey() {
|
|
@@ -104,6 +112,13 @@ export function loadInstalled() {
|
|
|
104
112
|
// R2-13: Basic shape validation
|
|
105
113
|
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
106
114
|
return {};
|
|
115
|
+
// R4-FIX #12: 兼容旧数据 — 将 number 类型的 installed_at 统一为 ISO string
|
|
116
|
+
for (const key of Object.keys(data)) {
|
|
117
|
+
const entry = data[key];
|
|
118
|
+
if (entry && typeof entry.installed_at === "number") {
|
|
119
|
+
entry.installed_at = new Date(entry.installed_at).toISOString();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
107
122
|
return data;
|
|
108
123
|
}
|
|
109
124
|
catch {
|
|
@@ -115,10 +130,7 @@ export function saveInstalled(registry) {
|
|
|
115
130
|
ensureConfigDir();
|
|
116
131
|
const path = getInstalledPath();
|
|
117
132
|
writeFileSync(path, JSON.stringify(registry, null, 2) + "\n", { mode: 0o600 });
|
|
118
|
-
|
|
119
|
-
chmodSync(path, 0o600);
|
|
120
|
-
}
|
|
121
|
-
catch { /* Windows fallback */ }
|
|
133
|
+
safeChmod(path, 0o600);
|
|
122
134
|
}
|
|
123
135
|
/** R1-13: Zero-fill before overwrite for defense-in-depth */
|
|
124
136
|
export function clearConfig() {
|
package/dist/lib/installer.js
CHANGED
|
@@ -54,6 +54,10 @@ export async function installSkill(client, skillId, packageFile, expectedSha256,
|
|
|
54
54
|
buffer = result.buffer;
|
|
55
55
|
actualSha256 = result.sha256;
|
|
56
56
|
spinner.succeed(` 已下载 ${ui.fileSize(buffer.length)}`);
|
|
57
|
+
// R4-FIX #3: 大包内存占用警告
|
|
58
|
+
if (result.warnLargeDownload) {
|
|
59
|
+
ui.warning(`包体积较大 (${ui.fileSize(buffer.length)}),内存占用较高`);
|
|
60
|
+
}
|
|
57
61
|
}
|
|
58
62
|
catch (err) {
|
|
59
63
|
spinner.fail(` 下载失败`);
|
|
@@ -176,6 +180,23 @@ export function rollbackSkill(skillId) {
|
|
|
176
180
|
if (backups.length === 0)
|
|
177
181
|
return false;
|
|
178
182
|
const latestBackup = join(installDir, backups[0]);
|
|
183
|
+
// R4-FIX #6: 回滚前验证备份目录完整性
|
|
184
|
+
try {
|
|
185
|
+
const backupStat = statSync(latestBackup);
|
|
186
|
+
if (!backupStat.isDirectory()) {
|
|
187
|
+
ui.warning("备份路径不是有效目录,回滚中止");
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const backupContents = readdirSync(latestBackup);
|
|
191
|
+
if (backupContents.length === 0) {
|
|
192
|
+
ui.warning("备份目录为空,回滚中止 — 可能备份已损坏");
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
ui.warning("无法验证备份完整性,回滚中止");
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
179
200
|
if (existsSync(skillDir)) {
|
|
180
201
|
rmSync(skillDir, { recursive: true, force: true });
|
|
181
202
|
}
|
|
@@ -244,6 +265,16 @@ function extractZip(buffer, targetDir) {
|
|
|
244
265
|
reject(new Error(`Unsafe path in ZIP: ${entry.fileName}`));
|
|
245
266
|
return;
|
|
246
267
|
}
|
|
268
|
+
// R4-FIX #2: 拒绝 Windows Alternate Data Streams (filename:stream)
|
|
269
|
+
// 排除 Windows 盘符 (如 C:\)
|
|
270
|
+
const pathParts = entryPath.split(sep);
|
|
271
|
+
for (const part of pathParts) {
|
|
272
|
+
if (part.includes(":") && !/^[A-Za-z]:$/.test(part)) {
|
|
273
|
+
zipfile.close();
|
|
274
|
+
reject(new Error(`不安全的路径(可能包含 Windows ADS): ${entry.fileName}`));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
247
278
|
// INS-02 FIX: Reject symlink entries (external attributes bit 0xA000)
|
|
248
279
|
const externalAttrs = (entry.externalFileAttributes >>> 16) & 0xFFFF;
|
|
249
280
|
const S_IFLNK = 0xA000;
|
package/dist/types.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface CliConfig {
|
|
|
22
22
|
}
|
|
23
23
|
export interface InstalledSkill {
|
|
24
24
|
version: string;
|
|
25
|
+
/** R4-FIX #12: 统一为 ISO 8601 字符串 (如 "2025-01-01T00:00:00.000Z") */
|
|
25
26
|
installed_at: string;
|
|
26
27
|
path: string;
|
|
27
28
|
sha256: string;
|
|
@@ -33,6 +34,6 @@ export interface InstalledRegistry {
|
|
|
33
34
|
export declare const DEFAULT_SERVER = "https://skill.dreamlogic-claw.com";
|
|
34
35
|
export declare const DEFAULT_INSTALL_DIR_NAME = "dreamlogic-skills";
|
|
35
36
|
export declare const CONFIG_DIR_NAME = ".dreamlogic";
|
|
36
|
-
export declare const CLI_VERSION = "2.0.
|
|
37
|
+
export declare const CLI_VERSION = "2.0.7";
|
|
37
38
|
export declare const CLI_NAME = "Dreamlogic CLI";
|
|
38
39
|
export declare const CLI_AUTHOR = "Dreamlogic-ai by MAJORNINE";
|
package/dist/types.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
export const DEFAULT_SERVER = "https://skill.dreamlogic-claw.com";
|
|
3
3
|
export const DEFAULT_INSTALL_DIR_NAME = "dreamlogic-skills";
|
|
4
4
|
export const CONFIG_DIR_NAME = ".dreamlogic";
|
|
5
|
-
export const CLI_VERSION = "2.0.
|
|
5
|
+
export const CLI_VERSION = "2.0.7";
|
|
6
6
|
export const CLI_NAME = "Dreamlogic CLI";
|
|
7
7
|
export const CLI_AUTHOR = "Dreamlogic-ai by MAJORNINE";
|