@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 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.1 的命令契约。旧的 `send/history/daemon-*` 命令已移除。
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 stat2 = await lstat4(path);
12299
- if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.size > 128) throw new Error("unsafe");
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.1",
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 stat2 = await handle.stat();
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, stat2.size, 6, AMR_NB_FRAME_BYTES);
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, stat2.size, 9, AMR_WB_FRAME_BYTES);
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 isAbsolute7, join as join13, resolve as resolve10 } from "node:path";
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, isAbsolute as isAbsolute6, join as join12, resolve as resolve9 } from "node:path";
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("UNSUPPORTED", "Output path is invalid.");
27073
+ throw new NativeOperationGatewayError("OUTPUT_PATH_INVALID", "Output path is invalid.");
27066
27074
  }
27067
27075
  const requested = resolve9(outputPath);
27068
- const entry = await lstat9(requested).catch((error) => {
27076
+ const entry = await stat2(requested).catch((error) => {
27069
27077
  if (error.code === "ENOENT") return void 0;
27070
- throw error;
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 lstat9(dirname5(destination));
27077
- if (!parent.isDirectory() || parent.isSymbolicLink() || typeof process.getuid === "function" && parent.uid !== process.getuid()) {
27078
- throw new NativeOperationGatewayError("UNSUPPORTED", "Output directory is unsafe.");
27079
- }
27080
- if (!isAbsolute6(destination)) {
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("UNSUPPORTED", "Output file already exists.");
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("UNSUPPORTED", "Output file already exists.");
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 (!isAbsolute7(outputPath) || outputPath.includes("\0")) {
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 (!isAbsolute7(input.sourcePath) || input.sourcePath.includes("\0")) {
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 isAbsolute8 } from "node:path";
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 (!isAbsolute8(value) || value.includes("\0")) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mu-cabin/muc-cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Standalone agent-first MUC CLI with an embedded background daemon",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 safe output path when required, and an idempotency key. Do not
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