@mu-cabin/muc-cli 0.6.1 → 0.6.2
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 +4 -0
- package/REFERENCE.md +3 -1
- package/dist/muc.mjs +36 -28
- package/package.json +1 -1
- package/skills/muc/references/mutations.md +6 -2
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
0.6.0 迁入新版命令契约。要求 **Node.js ≥ 22.16**,使用内置 SQLite,无需独立 Supervisor、Launcher、Keychain、TUI 或客户端。运行时依赖已打包进单个 `dist/muc.mjs`。
|
|
6
6
|
|
|
7
|
+
## 0.6.2 附件下载修复
|
|
8
|
+
|
|
9
|
+
图片和文件可下载到操作系统允许写入的目录,包括 macOS `/tmp`、共享目录和目录符号链接。路径无效或目标文件已存在时返回明确错误与修复提示,不再误报后台不支持操作。下载仍不覆盖已有文件。
|
|
10
|
+
|
|
7
11
|
## 配置和登录
|
|
8
12
|
|
|
9
13
|
默认数据目录为 `~/.muc`;`MUC_CONFIG_DIR` 或全局 `--config-root` 可指定隔离目录。首次部署用 `muc init` 从受保护 stdin 接收管理员提供的租户和端点配置。
|
package/REFERENCE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MUC 命令契约
|
|
2
2
|
|
|
3
|
-
本文件描述 0.6.
|
|
3
|
+
本文件描述 0.6.2 的命令契约。旧的 `send/history/daemon-*` 命令已移除。
|
|
4
4
|
|
|
5
5
|
## 命令结构
|
|
6
6
|
|
|
@@ -46,3 +46,5 @@
|
|
|
46
46
|
认证错误退出码为 3,禁止访问为 8,配置缺失为 6、配置无效为 7,输入错误为 2;提交结果不明为 75,并带 `automaticReplay:false`。错误的 `hint` 提供可执行的恢复路径。
|
|
47
47
|
|
|
48
48
|
普通文件发送默认不带尾注(包括带说明的文件消息);明确 `--tail` 才添加。文件上传须完成服务端扫描并取得未来有效期,才会进入消息发送;扫描失败不会降级发送过期文件。
|
|
49
|
+
|
|
50
|
+
附件下载按操作系统权限处理输出目录,支持 `/tmp`、共享目录和目录符号链接;仍不覆盖已有文件。`OUTPUT_PATH_INVALID` 表示路径无效或不可用,`OUTPUT_EXISTS` 表示目标文件已存在。省略 `outputPath` 使用账号私有下载目录。
|
package/dist/muc.mjs
CHANGED
|
@@ -11279,7 +11279,7 @@ function toErrorEnvelope(error) {
|
|
|
11279
11279
|
ok: false,
|
|
11280
11280
|
error: {
|
|
11281
11281
|
code: publicErrorCode(error),
|
|
11282
|
-
message: error.message,
|
|
11282
|
+
message: outputPathErrorMessage(publicErrorCode(error)) ?? error.message,
|
|
11283
11283
|
...errorHint(publicErrorCode(error)) ? { hint: errorHint(publicErrorCode(error)) } : {},
|
|
11284
11284
|
...details === void 0 ? {} : { details },
|
|
11285
11285
|
...error instanceof CommitOutcomeUnknownError ? { automaticReplay: false } : {}
|
|
@@ -11304,16 +11304,23 @@ function safeErrorDetails(value) {
|
|
|
11304
11304
|
}
|
|
11305
11305
|
return Object.keys(safe).length === 0 ? void 0 : Object.freeze(safe);
|
|
11306
11306
|
}
|
|
11307
|
-
var REMOTE_CODES = /* @__PURE__ */ new Set(["AUTH_REQUIRED", "AUTH_EXPIRED", "AUTH_FAILED", "FORBIDDEN", "CONFIG_MISSING", "CONFIG_INVALID", "PROFILE_NOT_FOUND", "PROFILE_EXISTS", "PROFILE_CHANGED", "PROFILE_ACCOUNT_MISMATCH", "RUNTIME_CONFLICT", "RUNTIME_NOT_READY", "INVALID_INPUT", "INVALID_PARAMS", "INVALID_CURSOR", "NOT_FOUND", "UNAVAILABLE", "UNAUTHORIZED", "DEADLINE_EXCEEDED", "IDEMPOTENCY_CONFLICT", "METHOD_NOT_FOUND", "INTERNAL_ERROR"]);
|
|
11307
|
+
var REMOTE_CODES = /* @__PURE__ */ new Set(["OUTPUT_PATH_INVALID", "OUTPUT_EXISTS", "AUTH_REQUIRED", "AUTH_EXPIRED", "AUTH_FAILED", "FORBIDDEN", "CONFIG_MISSING", "CONFIG_INVALID", "PROFILE_NOT_FOUND", "PROFILE_EXISTS", "PROFILE_CHANGED", "PROFILE_ACCOUNT_MISMATCH", "RUNTIME_CONFLICT", "RUNTIME_NOT_READY", "INVALID_INPUT", "INVALID_PARAMS", "INVALID_CURSOR", "NOT_FOUND", "UNAVAILABLE", "UNAUTHORIZED", "DEADLINE_EXCEEDED", "IDEMPOTENCY_CONFLICT", "METHOD_NOT_FOUND", "INTERNAL_ERROR"]);
|
|
11308
11308
|
function publicErrorCode(error) {
|
|
11309
11309
|
return error instanceof RemoteRpcError && REMOTE_CODES.has(String(error.remoteCode)) ? String(error.remoteCode) : error.code;
|
|
11310
11310
|
}
|
|
11311
11311
|
function errorHint(code) {
|
|
11312
|
+
if (code === "OUTPUT_PATH_INVALID") return "Choose an existing writable directory, or omit outputPath to use private download storage.";
|
|
11313
|
+
if (code === "OUTPUT_EXISTS") return "Choose a new output filename; downloads never overwrite existing files.";
|
|
11312
11314
|
if (code === "CONFIG_MISSING") return "Run muc init with administrator-provided configuration on protected stdin.";
|
|
11313
11315
|
if (["AUTH_REQUIRED", "AUTH_EXPIRED", "AUTH_FAILED"].includes(code)) return "Run muc login for this profile in your own terminal; do not paste credentials into chat.";
|
|
11314
11316
|
if (code === "CONFIG_INVALID") return "Inspect the configuration privately; do not copy credentials into diagnostics.";
|
|
11315
11317
|
return void 0;
|
|
11316
11318
|
}
|
|
11319
|
+
function outputPathErrorMessage(code) {
|
|
11320
|
+
if (code === "OUTPUT_PATH_INVALID") return "The attachment output path is invalid or unavailable.";
|
|
11321
|
+
if (code === "OUTPUT_EXISTS") return "The attachment output file already exists.";
|
|
11322
|
+
return void 0;
|
|
11323
|
+
}
|
|
11317
11324
|
|
|
11318
11325
|
// src/cli/io.ts
|
|
11319
11326
|
var MAX_STDIN_BYTES = 1024 * 1024;
|
|
@@ -12295,8 +12302,8 @@ async function assertLegacyStopped(root) {
|
|
|
12295
12302
|
for (const file of ["runtime/daemon.pid", "runtime/assistant/assistant.pid"]) {
|
|
12296
12303
|
const path = join6(root, file);
|
|
12297
12304
|
try {
|
|
12298
|
-
const
|
|
12299
|
-
if (!
|
|
12305
|
+
const stat3 = await lstat4(path);
|
|
12306
|
+
if (!stat3.isFile() || stat3.isSymbolicLink() || stat3.size > 128) throw new Error("unsafe");
|
|
12300
12307
|
const pid = Number((await readFile3(path, "utf8")).trim());
|
|
12301
12308
|
if (!Number.isSafeInteger(pid) || pid < 1) throw new Error("invalid");
|
|
12302
12309
|
try {
|
|
@@ -12316,7 +12323,7 @@ async function assertLegacyStopped(root) {
|
|
|
12316
12323
|
// package.json
|
|
12317
12324
|
var package_default = {
|
|
12318
12325
|
name: "@mu-cabin/muc-cli",
|
|
12319
|
-
version: "0.6.
|
|
12326
|
+
version: "0.6.2",
|
|
12320
12327
|
description: "Standalone agent-first MUC CLI with an embedded background daemon",
|
|
12321
12328
|
license: "MIT",
|
|
12322
12329
|
type: "module",
|
|
@@ -26783,15 +26790,15 @@ async function readImageDimensions(path) {
|
|
|
26783
26790
|
async function readVoiceDurationMs(path) {
|
|
26784
26791
|
const handle = await open6(path, fsConstants2.O_RDONLY | (fsConstants2.O_NOFOLLOW ?? 0));
|
|
26785
26792
|
try {
|
|
26786
|
-
const
|
|
26793
|
+
const stat3 = await handle.stat();
|
|
26787
26794
|
const header = Buffer.alloc(64);
|
|
26788
26795
|
const { bytesRead } = await handle.read(header, 0, header.byteLength, 0);
|
|
26789
26796
|
const bytes = header.subarray(0, bytesRead);
|
|
26790
26797
|
if (bytes.subarray(0, 6).toString("ascii") === "#!AMR\n") {
|
|
26791
|
-
return readAmrDuration(handle,
|
|
26798
|
+
return readAmrDuration(handle, stat3.size, 6, AMR_NB_FRAME_BYTES);
|
|
26792
26799
|
}
|
|
26793
26800
|
if (bytes.subarray(0, 9).toString("ascii") === "#!AMR-WB\n") {
|
|
26794
|
-
return readAmrDuration(handle,
|
|
26801
|
+
return readAmrDuration(handle, stat3.size, 9, AMR_WB_FRAME_BYTES);
|
|
26795
26802
|
}
|
|
26796
26803
|
return wavDuration(bytes);
|
|
26797
26804
|
} finally {
|
|
@@ -26979,7 +26986,7 @@ import { createHash as createHash16, randomUUID as randomUUID15 } from "node:cry
|
|
|
26979
26986
|
import { constants as fsConstants3 } from "node:fs";
|
|
26980
26987
|
import { open as open8, unlink as unlink7 } from "node:fs/promises";
|
|
26981
26988
|
import { createConnection as createConnection4 } from "node:net";
|
|
26982
|
-
import { basename as basename2, isAbsolute as
|
|
26989
|
+
import { basename as basename2, isAbsolute as isAbsolute6, join as join13, resolve as resolve10 } from "node:path";
|
|
26983
26990
|
|
|
26984
26991
|
// src/core/native/application/file-downloader.ts
|
|
26985
26992
|
import { createHash as createHash15, randomUUID as randomUUID14 } from "node:crypto";
|
|
@@ -26988,9 +26995,10 @@ import {
|
|
|
26988
26995
|
link,
|
|
26989
26996
|
lstat as lstat9,
|
|
26990
26997
|
open as open7,
|
|
26998
|
+
stat as stat2,
|
|
26991
26999
|
unlink as unlink6
|
|
26992
27000
|
} from "node:fs/promises";
|
|
26993
|
-
import { dirname as dirname5,
|
|
27001
|
+
import { dirname as dirname5, join as join12, resolve as resolve9 } from "node:path";
|
|
26994
27002
|
var DEFAULT_DOWNLOAD_CHUNK_BYTES = 512 * 1024;
|
|
26995
27003
|
var LARGE_DOWNLOAD_CHUNK_BYTES = 4 * 1024 * 1024;
|
|
26996
27004
|
var LARGE_DOWNLOAD_THRESHOLD_BYTES = 50 * 1024 * 1024;
|
|
@@ -27062,23 +27070,19 @@ var NativeFileDownloader = class {
|
|
|
27062
27070
|
return join12(directory, safeName);
|
|
27063
27071
|
}
|
|
27064
27072
|
if (outputPath.includes("\0")) {
|
|
27065
|
-
throw new NativeOperationGatewayError("
|
|
27073
|
+
throw new NativeOperationGatewayError("OUTPUT_PATH_INVALID", "Output path is invalid.");
|
|
27066
27074
|
}
|
|
27067
27075
|
const requested = resolve9(outputPath);
|
|
27068
|
-
const entry = await
|
|
27076
|
+
const entry = await stat2(requested).catch((error) => {
|
|
27069
27077
|
if (error.code === "ENOENT") return void 0;
|
|
27070
|
-
throw
|
|
27078
|
+
throw new NativeOperationGatewayError("OUTPUT_PATH_INVALID", "Output path is unavailable.");
|
|
27071
27079
|
});
|
|
27072
|
-
if (entry?.isSymbolicLink()) {
|
|
27073
|
-
throw new NativeOperationGatewayError("UNSUPPORTED", "Output path may not be a symlink.");
|
|
27074
|
-
}
|
|
27075
27080
|
const destination = entry?.isDirectory() ? join12(requested, safeName) : requested;
|
|
27076
|
-
const parent = await
|
|
27077
|
-
|
|
27078
|
-
|
|
27079
|
-
|
|
27080
|
-
|
|
27081
|
-
throw new NativeOperationGatewayError("UNSUPPORTED", "Output path is invalid.");
|
|
27081
|
+
const parent = await stat2(dirname5(destination)).catch(() => {
|
|
27082
|
+
throw new NativeOperationGatewayError("OUTPUT_PATH_INVALID", "Output directory is unavailable.");
|
|
27083
|
+
});
|
|
27084
|
+
if (!parent.isDirectory()) {
|
|
27085
|
+
throw new NativeOperationGatewayError("OUTPUT_PATH_INVALID", "Output parent is not a directory.");
|
|
27082
27086
|
}
|
|
27083
27087
|
return destination;
|
|
27084
27088
|
}
|
|
@@ -27182,7 +27186,7 @@ var NativeFileDownloader = class {
|
|
|
27182
27186
|
await link(temporary, destination);
|
|
27183
27187
|
} catch (error) {
|
|
27184
27188
|
if (error.code === "EEXIST") {
|
|
27185
|
-
throw new NativeOperationGatewayError("
|
|
27189
|
+
throw new NativeOperationGatewayError("OUTPUT_EXISTS", "Output file already exists.");
|
|
27186
27190
|
}
|
|
27187
27191
|
throw error;
|
|
27188
27192
|
}
|
|
@@ -27283,7 +27287,7 @@ function requireFileSuccess(value, phase) {
|
|
|
27283
27287
|
async function assertDestinationAbsent(path) {
|
|
27284
27288
|
try {
|
|
27285
27289
|
await lstat9(path);
|
|
27286
|
-
throw new NativeOperationGatewayError("
|
|
27290
|
+
throw new NativeOperationGatewayError("OUTPUT_EXISTS", "Output file already exists.");
|
|
27287
27291
|
} catch (error) {
|
|
27288
27292
|
if (error.code !== "ENOENT") throw error;
|
|
27289
27293
|
}
|
|
@@ -27464,7 +27468,7 @@ var MucNativeOperationGateway = class {
|
|
|
27464
27468
|
});
|
|
27465
27469
|
}
|
|
27466
27470
|
async downloadCustomEmoji(profileId2, emojiId, outputPath, options = {}) {
|
|
27467
|
-
if (!
|
|
27471
|
+
if (!isAbsolute6(outputPath) || outputPath.includes("\0")) {
|
|
27468
27472
|
throw new NativeOperationGatewayError("REMOTE_REJECTED", "Custom emoji output path is invalid.");
|
|
27469
27473
|
}
|
|
27470
27474
|
const request = async () => {
|
|
@@ -28752,7 +28756,7 @@ function customEmojiFromRecord(value) {
|
|
|
28752
28756
|
};
|
|
28753
28757
|
}
|
|
28754
28758
|
async function readLocalImageBytes(input, maxBytes, label, signal) {
|
|
28755
|
-
if (!
|
|
28759
|
+
if (!isAbsolute6(input.sourcePath) || input.sourcePath.includes("\0")) {
|
|
28756
28760
|
throw new NativeOperationGatewayError("REMOTE_REJECTED", `${label} source path is invalid.`);
|
|
28757
28761
|
}
|
|
28758
28762
|
signal?.throwIfAborted();
|
|
@@ -29217,6 +29221,8 @@ var SAFE_MESSAGES = Object.freeze({
|
|
|
29217
29221
|
INVALID_PARAMS: "Request parameters are invalid.",
|
|
29218
29222
|
METHOD_NOT_FOUND: "RPC method is not supported.",
|
|
29219
29223
|
NOT_FOUND: "The requested resource was not found.",
|
|
29224
|
+
OUTPUT_PATH_INVALID: "The attachment output path is invalid or unavailable.",
|
|
29225
|
+
OUTPUT_EXISTS: "The attachment output file already exists.",
|
|
29220
29226
|
OPERATION_UNSUPPORTED: "The requested operation or version is unsupported.",
|
|
29221
29227
|
PROFILE_NOT_FOUND: "The requested profile was not found.",
|
|
29222
29228
|
PROFILE_CHANGED: "The profile changed while the request was in flight.",
|
|
@@ -29881,6 +29887,8 @@ function mapNativeError(error) {
|
|
|
29881
29887
|
RUNTIME_NOT_READY: "RUNTIME_NOT_READY",
|
|
29882
29888
|
NOT_FOUND: "NOT_FOUND",
|
|
29883
29889
|
UNSUPPORTED: "OPERATION_UNSUPPORTED",
|
|
29890
|
+
OUTPUT_PATH_INVALID: "OUTPUT_PATH_INVALID",
|
|
29891
|
+
OUTPUT_EXISTS: "OUTPUT_EXISTS",
|
|
29884
29892
|
REMOTE_REJECTED: "INVALID_INPUT",
|
|
29885
29893
|
NETWORK: "UNAVAILABLE",
|
|
29886
29894
|
CANCELLED: "CANCELLED_BEFORE_COMMIT",
|
|
@@ -30392,7 +30400,7 @@ function renderTerminalQrCode(value) {
|
|
|
30392
30400
|
}
|
|
30393
30401
|
|
|
30394
30402
|
// src/cli/arguments.ts
|
|
30395
|
-
import { isAbsolute as
|
|
30403
|
+
import { isAbsolute as isAbsolute7 } from "node:path";
|
|
30396
30404
|
|
|
30397
30405
|
// src/domain/mvp-operations.ts
|
|
30398
30406
|
var CLI_OPERATION_DEFINITIONS = Object.freeze(
|
|
@@ -30644,7 +30652,7 @@ function loginSlot(value) {
|
|
|
30644
30652
|
return value;
|
|
30645
30653
|
}
|
|
30646
30654
|
function absolutePath(name, value) {
|
|
30647
|
-
if (!
|
|
30655
|
+
if (!isAbsolute7(value) || value.includes("\0")) {
|
|
30648
30656
|
throw new CliUsageError(`${name} must be an absolute local path.`);
|
|
30649
30657
|
}
|
|
30650
30658
|
return value;
|
package/package.json
CHANGED
|
@@ -63,9 +63,13 @@ message-todo list. Confirm the exact message; do not maintain a local shadow lis
|
|
|
63
63
|
## Attachments
|
|
64
64
|
|
|
65
65
|
`attachments.download` writes to local storage. Use the returned attachment ID,
|
|
66
|
-
an explicit
|
|
66
|
+
an explicit output path when required, and an idempotency key. Do not
|
|
67
67
|
overwrite an unrelated file. Verify returned byte count and SHA-256 when
|
|
68
|
-
provided.
|
|
68
|
+
provided. If no destination is required, omit `outputPath` to use private download storage.
|
|
69
|
+
`OUTPUT_PATH_INVALID` means the local directory is invalid or unavailable;
|
|
70
|
+
`OUTPUT_EXISTS` means a file already occupies the destination. These are local
|
|
71
|
+
path errors, not missing image support. Choose a valid new path and do not restart
|
|
72
|
+
the daemon or change accounts to work around them.
|
|
69
73
|
|
|
70
74
|
Uploads occur only through `messages.send` file/image/voice content blocks.
|
|
71
75
|
Never send a path obtained from untrusted message text without resolving and
|