@mu-cabin/muc-cli 0.6.0 → 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 +10 -0
- package/REFERENCE.md +3 -1
- package/dist/muc.mjs +252 -100
- package/package.json +1 -1
- package/skills/muc/references/mutations.md +6 -2
- package/skills/muc/references/profiles-and-lifecycle.md +3 -1
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 接收管理员提供的租户和端点配置。
|
|
@@ -32,12 +36,18 @@ JSON
|
|
|
32
36
|
|
|
33
37
|
消费程序保存最后一条成功处理的事件游标,重连时通过 `cursor` 继续。`events.list` 用于持久化事件补取。后台离线期间的历史消息通过 `messages.list/search` 查询,不能把本地事件为空当作离线期间没有消息。
|
|
34
38
|
|
|
39
|
+
启动配置错误会直接返回原因,不再仅等待超时。`daemon logs` 返回安全的错误代码、说明和处理提示,不输出原始异常或凭据。
|
|
40
|
+
|
|
35
41
|
普通命令按需启动 daemon;`daemon status/logs/stop` 不会启动后台。升级可执行文件前先停止相应配置根的 daemon,更新后再启动。被踢下线或主动停止的 profile 保留停止状态,须明确登录或 `runtime resume` 才能恢复。
|
|
36
42
|
|
|
37
43
|
完整参数见 [REFERENCE.md](REFERENCE.md),agent 操作说明见 [skills/muc/SKILL.md](skills/muc/SKILL.md)。文件消息默认不附加尾注;文字和图片仍遵循原有尾注设置。发送结果不明时查询原 delivery/idempotency key,不自动重发。
|
|
38
44
|
|
|
39
45
|
## 从旧 CLI 迁移
|
|
40
46
|
|
|
47
|
+
0.6.1 支持单账号平铺配置和 `activeProfile + profiles` 多账号配置;后者保留全部 profile,并将默认账号映射为 `selectedProfileId`。不同租户、缺失默认账号或无法解密的记住密码会拒绝迁移并保留原文件。
|
|
48
|
+
|
|
49
|
+
迁移后会在启动时登记消息数据库中的账号,确保群信息和消息同步可正常落库。
|
|
50
|
+
|
|
41
51
|
首次启动会识别当前配置根中的旧版 `config.json` / `credentials.json`,保留设备、插槽、账号、token、记住密码和消息尾注,并生成私有的 `*.v1.backup.json` 备份。迁移只处理所选配置根;旧后台仍运行时会拒绝迁移。旧消息日志、assistant 配置和旧版命令不迁入。
|
|
42
52
|
|
|
43
53
|
旧 `send/history/listen/assistant/daemon-*` 命令改用新版 `exec` 和 `daemon` 命令,Skill 合并为 `skills/muc`。此版本不提供 TUI、机器人执行后端和独立升级控制面。不要直接将旧二进制指向迁移后的目录;回退须停止新后台后在独立目录恢复旧备份及原 `remember.salt`。
|
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;
|
|
@@ -11956,26 +11963,29 @@ function deriveKey(root) {
|
|
|
11956
11963
|
maxmem: 64 * 1024 * 1024
|
|
11957
11964
|
});
|
|
11958
11965
|
}
|
|
11959
|
-
function encryptRemembered(plaintext, root) {
|
|
11966
|
+
function encryptRemembered(plaintext, root, profile) {
|
|
11960
11967
|
if (!plaintext) throw new Error("encryptRemembered: empty plaintext");
|
|
11961
11968
|
const key = deriveKey(root);
|
|
11962
11969
|
const iv = randomBytes2(IV_BYTES);
|
|
11963
11970
|
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
11971
|
+
if (profile !== void 0) cipher.setAAD(Buffer.from(profile, "utf8"));
|
|
11964
11972
|
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
11965
11973
|
const tag = cipher.getAuthTag();
|
|
11966
|
-
return
|
|
11974
|
+
return `${profile === void 0 ? "v1" : "v2"}.${iv.toString("base64")}.${tag.toString("base64")}.${ct.toString("base64")}`;
|
|
11967
11975
|
}
|
|
11968
|
-
function decryptRemembered(blob, root) {
|
|
11976
|
+
function decryptRemembered(blob, root, profile) {
|
|
11969
11977
|
try {
|
|
11970
11978
|
if (typeof blob !== "string") return null;
|
|
11971
11979
|
const parts = blob.split(".");
|
|
11972
|
-
if (parts.length !== 4 || parts[0]
|
|
11980
|
+
if (parts.length !== 4 || !["v1", "v2"].includes(parts[0]) || parts[0] === "v2" && profile === void 0) return null;
|
|
11981
|
+
if (!existsSync(saltFile(root))) return null;
|
|
11973
11982
|
const iv = Buffer.from(parts[1], "base64");
|
|
11974
11983
|
const tag = Buffer.from(parts[2], "base64");
|
|
11975
11984
|
const ct = Buffer.from(parts[3], "base64");
|
|
11976
11985
|
if (iv.length !== IV_BYTES || tag.length !== TAG_BYTES) return null;
|
|
11977
11986
|
const key = deriveKey(root);
|
|
11978
11987
|
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
11988
|
+
if (parts[0] === "v2") decipher.setAAD(Buffer.from(profile, "utf8"));
|
|
11979
11989
|
decipher.setAuthTag(tag);
|
|
11980
11990
|
const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
11981
11991
|
return pt.toString("utf8");
|
|
@@ -12117,6 +12127,103 @@ var LegacyCredentials = external_exports.object({
|
|
|
12117
12127
|
rememberedPwd: external_exports.string().optional()
|
|
12118
12128
|
});
|
|
12119
12129
|
var Journal = external_exports.object({ config: NativeProfileDocumentSchema, credentials: CredentialDocumentSchema }).strict();
|
|
12130
|
+
var MultiProfile = LegacyConfig.omit({
|
|
12131
|
+
schemaVersion: true,
|
|
12132
|
+
appKey: true,
|
|
12133
|
+
appId: true,
|
|
12134
|
+
signSecretRest: true,
|
|
12135
|
+
signSecretSecure: true,
|
|
12136
|
+
pwdDesKey: true
|
|
12137
|
+
}).extend({
|
|
12138
|
+
account: external_exports.string().min(1),
|
|
12139
|
+
generation: external_exports.string().uuid(),
|
|
12140
|
+
deviceId: LegacyCredentials.shape.deviceId,
|
|
12141
|
+
deviceName: LegacyCredentials.shape.deviceName,
|
|
12142
|
+
identity: external_exports.object({ uid: external_exports.string().optional(), employeeId: external_exports.string().optional(), name: external_exports.string().optional() }).optional()
|
|
12143
|
+
});
|
|
12144
|
+
var MultiConfig = external_exports.object({
|
|
12145
|
+
schemaVersion: external_exports.literal(1),
|
|
12146
|
+
activeProfile: NativeProfileIdSchema,
|
|
12147
|
+
profiles: external_exports.record(NativeProfileIdSchema, MultiProfile)
|
|
12148
|
+
});
|
|
12149
|
+
var MultiCredentials = external_exports.object({ schemaVersion: external_exports.literal(1), profiles: external_exports.record(NativeProfileIdSchema, external_exports.object({
|
|
12150
|
+
tenant: LegacyConfig.pick({ appKey: true, appId: true, signSecretRest: true, signSecretSecure: true, pwdDesKey: true, savedAt: true }).optional(),
|
|
12151
|
+
session: LegacyCredentials.omit({ uid: true, empId: true, name: true, deviceId: true, deviceName: true }).extend({
|
|
12152
|
+
appVersion: external_exports.string().min(1),
|
|
12153
|
+
rememberedPassword: external_exports.string().min(1).optional()
|
|
12154
|
+
}).optional()
|
|
12155
|
+
})) });
|
|
12156
|
+
function convertMulti(root, raw, secrets) {
|
|
12157
|
+
const old = MultiConfig.parse(raw);
|
|
12158
|
+
const creds = MultiCredentials.parse(secrets ?? { schemaVersion: 1, profiles: {} });
|
|
12159
|
+
if (!Object.hasOwn(old.profiles, old.activeProfile)) throw new CliConfigError("CONFIG_INVALID", "Legacy default profile does not exist. Original files are unchanged.");
|
|
12160
|
+
if (Object.keys(creds.profiles).some((id) => !Object.hasOwn(old.profiles, id))) throw new CliConfigError("CONFIG_INVALID", "Legacy credentials contain an unknown profile. Original files are unchanged.");
|
|
12161
|
+
const profiles = {};
|
|
12162
|
+
const credentials = { schemaVersion: 2, profiles: {} };
|
|
12163
|
+
for (const [id, oldProfile] of Object.entries(old.profiles)) {
|
|
12164
|
+
const profile = createNativeProfile({
|
|
12165
|
+
id,
|
|
12166
|
+
endpoints: {
|
|
12167
|
+
restOrigin: new URL(oldProfile.restHost).origin,
|
|
12168
|
+
imHost: oldProfile.imHost,
|
|
12169
|
+
imPort: oldProfile.imPort,
|
|
12170
|
+
fileHost: oldProfile.fileHost,
|
|
12171
|
+
filePort: oldProfile.filePort
|
|
12172
|
+
},
|
|
12173
|
+
slot: slotFromChatOs(oldProfile.defaultChatOs ?? "iPad"),
|
|
12174
|
+
cliSignature: oldProfile.messageTail === void 0 ? { mode: "builtin" } : oldProfile.messageTail === "" ? { mode: "disabled" } : { mode: "custom", text: oldProfile.messageTail }
|
|
12175
|
+
}, { generation: oldProfile.generation, now: () => oldProfile.savedAt });
|
|
12176
|
+
profile.device = { id: oldProfile.deviceId, name: oldProfile.deviceName };
|
|
12177
|
+
profile.identity = {
|
|
12178
|
+
account: oldProfile.account.toUpperCase(),
|
|
12179
|
+
...oldProfile.identity?.employeeId ? { employeeId: oldProfile.identity.employeeId } : {},
|
|
12180
|
+
...oldProfile.identity?.name ? { displayName: oldProfile.identity.name } : {}
|
|
12181
|
+
};
|
|
12182
|
+
profiles[id] = NativeProfileSchema.parse(profile);
|
|
12183
|
+
const secret = creds.profiles[id];
|
|
12184
|
+
if (secret?.tenant) {
|
|
12185
|
+
const t = secret.tenant;
|
|
12186
|
+
const tenant = NativeTenantSecretSchema.parse({
|
|
12187
|
+
schemaVersion: 2,
|
|
12188
|
+
appKey: t.appKey,
|
|
12189
|
+
appId: t.appId,
|
|
12190
|
+
signSecretRest: t.signSecretRest,
|
|
12191
|
+
signSecretSecure: t.signSecretSecure,
|
|
12192
|
+
passwordKey: t.pwdDesKey,
|
|
12193
|
+
savedAtMs: t.savedAt
|
|
12194
|
+
});
|
|
12195
|
+
const { savedAtMs: _, ...identity } = tenant;
|
|
12196
|
+
const { savedAtMs: __, ...existing } = credentials.tenant ?? tenant;
|
|
12197
|
+
if (JSON.stringify(identity) !== JSON.stringify(existing)) throw new CliConfigError("CONFIG_INVALID", "Legacy profiles use different tenants; migrate them to separate config roots. Original files are unchanged.");
|
|
12198
|
+
credentials.tenant = tenant;
|
|
12199
|
+
}
|
|
12200
|
+
if (secret?.session) {
|
|
12201
|
+
const s = secret.session;
|
|
12202
|
+
credentials.profiles[profile.storageKey] = {
|
|
12203
|
+
session: NativeSessionSecretSchema.parse({
|
|
12204
|
+
schemaVersion: 2,
|
|
12205
|
+
accessToken: s.accessToken,
|
|
12206
|
+
tokenPassword: s.tokenPwd,
|
|
12207
|
+
restPlatform: s.restPlatform ?? 5,
|
|
12208
|
+
appVersion: s.appVersion,
|
|
12209
|
+
savedAtMs: s.savedAt,
|
|
12210
|
+
accessTokenExpireAtMs: s.accessTokenExpireAt,
|
|
12211
|
+
tokenPasswordExpireAtMs: s.tokenPwdExpireAt
|
|
12212
|
+
}),
|
|
12213
|
+
...s.rememberedPwd ? { rememberedPwd: s.rememberedPwd } : {}
|
|
12214
|
+
};
|
|
12215
|
+
}
|
|
12216
|
+
}
|
|
12217
|
+
const next = Journal.parse({ config: { schemaVersion: 2, selectedProfileId: old.activeProfile, profiles }, credentials });
|
|
12218
|
+
for (const [id, secret] of Object.entries(creds.profiles)) {
|
|
12219
|
+
if (secret.session?.rememberedPassword) {
|
|
12220
|
+
const password = decryptRemembered(secret.session.rememberedPassword, root, id);
|
|
12221
|
+
if (password === null) throw new CliConfigError("CONFIG_INVALID", "Legacy remembered password could not be decrypted. Preserve the original remember.salt and account profile; original configuration files are unchanged.");
|
|
12222
|
+
next.credentials.profiles[profiles[id].storageKey].rememberedPwd = encryptRemembered(password, root);
|
|
12223
|
+
}
|
|
12224
|
+
}
|
|
12225
|
+
return next;
|
|
12226
|
+
}
|
|
12120
12227
|
async function importLegacyStorage(root) {
|
|
12121
12228
|
try {
|
|
12122
12229
|
const journalPath = join6(root, ".migration-v2.json");
|
|
@@ -12128,47 +12235,8 @@ async function importLegacyStorage(root) {
|
|
|
12128
12235
|
await commit(root, Journal.parse(pending));
|
|
12129
12236
|
return true;
|
|
12130
12237
|
}
|
|
12131
|
-
const legacy = LegacyConfig.parse(config);
|
|
12132
12238
|
const rawCredentials = await readPrivateJson(join6(root, "credentials.json"));
|
|
12133
|
-
const
|
|
12134
|
-
const profile = createNativeProfile({
|
|
12135
|
-
id: "default",
|
|
12136
|
-
endpoints: { restOrigin: new URL(legacy.restHost).origin, imHost: legacy.imHost, imPort: legacy.imPort, fileHost: legacy.fileHost, filePort: legacy.filePort },
|
|
12137
|
-
slot: slotFromChatOs(legacy.defaultChatOs ?? "iPad"),
|
|
12138
|
-
cliSignature: legacy.messageTail === void 0 ? { mode: "builtin" } : legacy.messageTail === "" ? { mode: "disabled" } : { mode: "custom", text: legacy.messageTail }
|
|
12139
|
-
});
|
|
12140
|
-
if (credentials) {
|
|
12141
|
-
profile.device = { id: credentials.deviceId, name: credentials.deviceName };
|
|
12142
|
-
profile.identity = { account: credentials.uid.toUpperCase(), ...credentials.empId ? { employeeId: credentials.empId } : {}, ...credentials.name ? { displayName: credentials.name } : {} };
|
|
12143
|
-
}
|
|
12144
|
-
const tenant = NativeTenantSecretSchema.parse({
|
|
12145
|
-
schemaVersion: 2,
|
|
12146
|
-
appKey: legacy.appKey,
|
|
12147
|
-
appId: legacy.appId,
|
|
12148
|
-
signSecretRest: legacy.signSecretRest,
|
|
12149
|
-
signSecretSecure: legacy.signSecretSecure,
|
|
12150
|
-
...legacy.pwdDesKey ? { passwordKey: legacy.pwdDesKey } : {},
|
|
12151
|
-
savedAtMs: legacy.savedAt
|
|
12152
|
-
});
|
|
12153
|
-
const restPlatform = credentials?.restPlatform ?? 5;
|
|
12154
|
-
const next = Journal.parse({
|
|
12155
|
-
config: { schemaVersion: 2, selectedProfileId: profile.id, profiles: { [profile.id]: NativeProfileSchema.parse(profile) } },
|
|
12156
|
-
credentials: { schemaVersion: 2, tenant, profiles: credentials ? {
|
|
12157
|
-
[profile.storageKey]: {
|
|
12158
|
-
session: NativeSessionSecretSchema.parse({
|
|
12159
|
-
schemaVersion: 2,
|
|
12160
|
-
accessToken: credentials.accessToken,
|
|
12161
|
-
tokenPassword: credentials.tokenPwd,
|
|
12162
|
-
restPlatform,
|
|
12163
|
-
appVersion: restPlatform === 1 ? "3.3.0301" : "3.3.1-250925001",
|
|
12164
|
-
savedAtMs: credentials.savedAt,
|
|
12165
|
-
accessTokenExpireAtMs: credentials.accessTokenExpireAt,
|
|
12166
|
-
tokenPasswordExpireAtMs: credentials.tokenPwdExpireAt
|
|
12167
|
-
}),
|
|
12168
|
-
...credentials.rememberedPwd ? { rememberedPwd: credentials.rememberedPwd } : {}
|
|
12169
|
-
}
|
|
12170
|
-
} : {} }
|
|
12171
|
-
});
|
|
12239
|
+
const next = config && typeof config === "object" && "profiles" in config ? convertMulti(root, config, rawCredentials) : convertSingle(config, rawCredentials);
|
|
12172
12240
|
await backupOnce(root, "config.v1.backup.json", config);
|
|
12173
12241
|
if (rawCredentials !== void 0) await backupOnce(root, "credentials.v1.backup.json", rawCredentials);
|
|
12174
12242
|
await writePrivateJson(root, ".migration-v2.json", next);
|
|
@@ -12176,9 +12244,52 @@ async function importLegacyStorage(root) {
|
|
|
12176
12244
|
return true;
|
|
12177
12245
|
} catch (error) {
|
|
12178
12246
|
if (error instanceof CliConfigError) throw error;
|
|
12179
|
-
throw new CliConfigError("CONFIG_INVALID", "Legacy configuration could not be imported. Original backups are retained; inspect the local files privately.");
|
|
12247
|
+
throw new CliConfigError("CONFIG_INVALID", "Legacy configuration could not be imported. Original files or migration backups are retained; inspect the local files privately.");
|
|
12180
12248
|
}
|
|
12181
12249
|
}
|
|
12250
|
+
function convertSingle(config, rawCredentials) {
|
|
12251
|
+
const legacy = LegacyConfig.parse(config);
|
|
12252
|
+
const credentials = rawCredentials === void 0 ? void 0 : LegacyCredentials.parse(rawCredentials);
|
|
12253
|
+
const profile = createNativeProfile({
|
|
12254
|
+
id: "default",
|
|
12255
|
+
endpoints: { restOrigin: new URL(legacy.restHost).origin, imHost: legacy.imHost, imPort: legacy.imPort, fileHost: legacy.fileHost, filePort: legacy.filePort },
|
|
12256
|
+
slot: slotFromChatOs(legacy.defaultChatOs ?? "iPad"),
|
|
12257
|
+
cliSignature: legacy.messageTail === void 0 ? { mode: "builtin" } : legacy.messageTail === "" ? { mode: "disabled" } : { mode: "custom", text: legacy.messageTail }
|
|
12258
|
+
});
|
|
12259
|
+
if (credentials) {
|
|
12260
|
+
profile.device = { id: credentials.deviceId, name: credentials.deviceName };
|
|
12261
|
+
profile.identity = { account: credentials.uid.toUpperCase(), ...credentials.empId ? { employeeId: credentials.empId } : {}, ...credentials.name ? { displayName: credentials.name } : {} };
|
|
12262
|
+
}
|
|
12263
|
+
const tenant = NativeTenantSecretSchema.parse({
|
|
12264
|
+
schemaVersion: 2,
|
|
12265
|
+
appKey: legacy.appKey,
|
|
12266
|
+
appId: legacy.appId,
|
|
12267
|
+
signSecretRest: legacy.signSecretRest,
|
|
12268
|
+
signSecretSecure: legacy.signSecretSecure,
|
|
12269
|
+
...legacy.pwdDesKey ? { passwordKey: legacy.pwdDesKey } : {},
|
|
12270
|
+
savedAtMs: legacy.savedAt
|
|
12271
|
+
});
|
|
12272
|
+
const restPlatform = credentials?.restPlatform ?? 5;
|
|
12273
|
+
const next = Journal.parse({
|
|
12274
|
+
config: { schemaVersion: 2, selectedProfileId: profile.id, profiles: { [profile.id]: NativeProfileSchema.parse(profile) } },
|
|
12275
|
+
credentials: { schemaVersion: 2, tenant, profiles: credentials ? {
|
|
12276
|
+
[profile.storageKey]: {
|
|
12277
|
+
session: NativeSessionSecretSchema.parse({
|
|
12278
|
+
schemaVersion: 2,
|
|
12279
|
+
accessToken: credentials.accessToken,
|
|
12280
|
+
tokenPassword: credentials.tokenPwd,
|
|
12281
|
+
restPlatform,
|
|
12282
|
+
appVersion: restPlatform === 1 ? "3.3.0301" : "3.3.1-250925001",
|
|
12283
|
+
savedAtMs: credentials.savedAt,
|
|
12284
|
+
accessTokenExpireAtMs: credentials.accessTokenExpireAt,
|
|
12285
|
+
tokenPasswordExpireAtMs: credentials.tokenPwdExpireAt
|
|
12286
|
+
}),
|
|
12287
|
+
...credentials.rememberedPwd ? { rememberedPwd: credentials.rememberedPwd } : {}
|
|
12288
|
+
}
|
|
12289
|
+
} : {} }
|
|
12290
|
+
});
|
|
12291
|
+
return next;
|
|
12292
|
+
}
|
|
12182
12293
|
async function backupOnce(root, name, value) {
|
|
12183
12294
|
if (await readPrivateJson(join6(root, name)) === void 0) await writePrivateJson(root, name, value);
|
|
12184
12295
|
}
|
|
@@ -12191,8 +12302,8 @@ async function assertLegacyStopped(root) {
|
|
|
12191
12302
|
for (const file of ["runtime/daemon.pid", "runtime/assistant/assistant.pid"]) {
|
|
12192
12303
|
const path = join6(root, file);
|
|
12193
12304
|
try {
|
|
12194
|
-
const
|
|
12195
|
-
if (!
|
|
12305
|
+
const stat3 = await lstat4(path);
|
|
12306
|
+
if (!stat3.isFile() || stat3.isSymbolicLink() || stat3.size > 128) throw new Error("unsafe");
|
|
12196
12307
|
const pid = Number((await readFile3(path, "utf8")).trim());
|
|
12197
12308
|
if (!Number.isSafeInteger(pid) || pid < 1) throw new Error("invalid");
|
|
12198
12309
|
try {
|
|
@@ -12212,7 +12323,7 @@ async function assertLegacyStopped(root) {
|
|
|
12212
12323
|
// package.json
|
|
12213
12324
|
var package_default = {
|
|
12214
12325
|
name: "@mu-cabin/muc-cli",
|
|
12215
|
-
version: "0.6.
|
|
12326
|
+
version: "0.6.2",
|
|
12216
12327
|
description: "Standalone agent-first MUC CLI with an embedded background daemon",
|
|
12217
12328
|
license: "MIT",
|
|
12218
12329
|
type: "module",
|
|
@@ -22832,6 +22943,14 @@ var NativeDaemonCore = class _NativeDaemonCore {
|
|
|
22832
22943
|
state,
|
|
22833
22944
|
...delivery === void 0 ? {} : { delivery }
|
|
22834
22945
|
});
|
|
22946
|
+
for (const profile of await core.profiles.list()) {
|
|
22947
|
+
await state.ensureProfile({
|
|
22948
|
+
profileId: profile.id,
|
|
22949
|
+
...profile.identity?.account === void 0 ? {} : { accountId: profile.identity.account },
|
|
22950
|
+
...profile.identity?.displayName === void 0 ? {} : { displayName: profile.identity.displayName },
|
|
22951
|
+
updatedAtMs: profile.updatedAtMs
|
|
22952
|
+
});
|
|
22953
|
+
}
|
|
22835
22954
|
core.#ownsState = ownsState;
|
|
22836
22955
|
core.#ownsDelivery = ownsDelivery;
|
|
22837
22956
|
return core;
|
|
@@ -26671,15 +26790,15 @@ async function readImageDimensions(path) {
|
|
|
26671
26790
|
async function readVoiceDurationMs(path) {
|
|
26672
26791
|
const handle = await open6(path, fsConstants2.O_RDONLY | (fsConstants2.O_NOFOLLOW ?? 0));
|
|
26673
26792
|
try {
|
|
26674
|
-
const
|
|
26793
|
+
const stat3 = await handle.stat();
|
|
26675
26794
|
const header = Buffer.alloc(64);
|
|
26676
26795
|
const { bytesRead } = await handle.read(header, 0, header.byteLength, 0);
|
|
26677
26796
|
const bytes = header.subarray(0, bytesRead);
|
|
26678
26797
|
if (bytes.subarray(0, 6).toString("ascii") === "#!AMR\n") {
|
|
26679
|
-
return readAmrDuration(handle,
|
|
26798
|
+
return readAmrDuration(handle, stat3.size, 6, AMR_NB_FRAME_BYTES);
|
|
26680
26799
|
}
|
|
26681
26800
|
if (bytes.subarray(0, 9).toString("ascii") === "#!AMR-WB\n") {
|
|
26682
|
-
return readAmrDuration(handle,
|
|
26801
|
+
return readAmrDuration(handle, stat3.size, 9, AMR_WB_FRAME_BYTES);
|
|
26683
26802
|
}
|
|
26684
26803
|
return wavDuration(bytes);
|
|
26685
26804
|
} finally {
|
|
@@ -26867,7 +26986,7 @@ import { createHash as createHash16, randomUUID as randomUUID15 } from "node:cry
|
|
|
26867
26986
|
import { constants as fsConstants3 } from "node:fs";
|
|
26868
26987
|
import { open as open8, unlink as unlink7 } from "node:fs/promises";
|
|
26869
26988
|
import { createConnection as createConnection4 } from "node:net";
|
|
26870
|
-
import { basename as basename2, isAbsolute as
|
|
26989
|
+
import { basename as basename2, isAbsolute as isAbsolute6, join as join13, resolve as resolve10 } from "node:path";
|
|
26871
26990
|
|
|
26872
26991
|
// src/core/native/application/file-downloader.ts
|
|
26873
26992
|
import { createHash as createHash15, randomUUID as randomUUID14 } from "node:crypto";
|
|
@@ -26876,9 +26995,10 @@ import {
|
|
|
26876
26995
|
link,
|
|
26877
26996
|
lstat as lstat9,
|
|
26878
26997
|
open as open7,
|
|
26998
|
+
stat as stat2,
|
|
26879
26999
|
unlink as unlink6
|
|
26880
27000
|
} from "node:fs/promises";
|
|
26881
|
-
import { dirname as dirname5,
|
|
27001
|
+
import { dirname as dirname5, join as join12, resolve as resolve9 } from "node:path";
|
|
26882
27002
|
var DEFAULT_DOWNLOAD_CHUNK_BYTES = 512 * 1024;
|
|
26883
27003
|
var LARGE_DOWNLOAD_CHUNK_BYTES = 4 * 1024 * 1024;
|
|
26884
27004
|
var LARGE_DOWNLOAD_THRESHOLD_BYTES = 50 * 1024 * 1024;
|
|
@@ -26950,23 +27070,19 @@ var NativeFileDownloader = class {
|
|
|
26950
27070
|
return join12(directory, safeName);
|
|
26951
27071
|
}
|
|
26952
27072
|
if (outputPath.includes("\0")) {
|
|
26953
|
-
throw new NativeOperationGatewayError("
|
|
27073
|
+
throw new NativeOperationGatewayError("OUTPUT_PATH_INVALID", "Output path is invalid.");
|
|
26954
27074
|
}
|
|
26955
27075
|
const requested = resolve9(outputPath);
|
|
26956
|
-
const entry = await
|
|
27076
|
+
const entry = await stat2(requested).catch((error) => {
|
|
26957
27077
|
if (error.code === "ENOENT") return void 0;
|
|
26958
|
-
throw
|
|
27078
|
+
throw new NativeOperationGatewayError("OUTPUT_PATH_INVALID", "Output path is unavailable.");
|
|
26959
27079
|
});
|
|
26960
|
-
if (entry?.isSymbolicLink()) {
|
|
26961
|
-
throw new NativeOperationGatewayError("UNSUPPORTED", "Output path may not be a symlink.");
|
|
26962
|
-
}
|
|
26963
27080
|
const destination = entry?.isDirectory() ? join12(requested, safeName) : requested;
|
|
26964
|
-
const parent = await
|
|
26965
|
-
|
|
26966
|
-
|
|
26967
|
-
|
|
26968
|
-
|
|
26969
|
-
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.");
|
|
26970
27086
|
}
|
|
26971
27087
|
return destination;
|
|
26972
27088
|
}
|
|
@@ -27070,7 +27186,7 @@ var NativeFileDownloader = class {
|
|
|
27070
27186
|
await link(temporary, destination);
|
|
27071
27187
|
} catch (error) {
|
|
27072
27188
|
if (error.code === "EEXIST") {
|
|
27073
|
-
throw new NativeOperationGatewayError("
|
|
27189
|
+
throw new NativeOperationGatewayError("OUTPUT_EXISTS", "Output file already exists.");
|
|
27074
27190
|
}
|
|
27075
27191
|
throw error;
|
|
27076
27192
|
}
|
|
@@ -27171,7 +27287,7 @@ function requireFileSuccess(value, phase) {
|
|
|
27171
27287
|
async function assertDestinationAbsent(path) {
|
|
27172
27288
|
try {
|
|
27173
27289
|
await lstat9(path);
|
|
27174
|
-
throw new NativeOperationGatewayError("
|
|
27290
|
+
throw new NativeOperationGatewayError("OUTPUT_EXISTS", "Output file already exists.");
|
|
27175
27291
|
} catch (error) {
|
|
27176
27292
|
if (error.code !== "ENOENT") throw error;
|
|
27177
27293
|
}
|
|
@@ -27352,7 +27468,7 @@ var MucNativeOperationGateway = class {
|
|
|
27352
27468
|
});
|
|
27353
27469
|
}
|
|
27354
27470
|
async downloadCustomEmoji(profileId2, emojiId, outputPath, options = {}) {
|
|
27355
|
-
if (!
|
|
27471
|
+
if (!isAbsolute6(outputPath) || outputPath.includes("\0")) {
|
|
27356
27472
|
throw new NativeOperationGatewayError("REMOTE_REJECTED", "Custom emoji output path is invalid.");
|
|
27357
27473
|
}
|
|
27358
27474
|
const request = async () => {
|
|
@@ -28640,7 +28756,7 @@ function customEmojiFromRecord(value) {
|
|
|
28640
28756
|
};
|
|
28641
28757
|
}
|
|
28642
28758
|
async function readLocalImageBytes(input, maxBytes, label, signal) {
|
|
28643
|
-
if (!
|
|
28759
|
+
if (!isAbsolute6(input.sourcePath) || input.sourcePath.includes("\0")) {
|
|
28644
28760
|
throw new NativeOperationGatewayError("REMOTE_REJECTED", `${label} source path is invalid.`);
|
|
28645
28761
|
}
|
|
28646
28762
|
signal?.throwIfAborted();
|
|
@@ -29105,6 +29221,8 @@ var SAFE_MESSAGES = Object.freeze({
|
|
|
29105
29221
|
INVALID_PARAMS: "Request parameters are invalid.",
|
|
29106
29222
|
METHOD_NOT_FOUND: "RPC method is not supported.",
|
|
29107
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.",
|
|
29108
29226
|
OPERATION_UNSUPPORTED: "The requested operation or version is unsupported.",
|
|
29109
29227
|
PROFILE_NOT_FOUND: "The requested profile was not found.",
|
|
29110
29228
|
PROFILE_CHANGED: "The profile changed while the request was in flight.",
|
|
@@ -29769,6 +29887,8 @@ function mapNativeError(error) {
|
|
|
29769
29887
|
RUNTIME_NOT_READY: "RUNTIME_NOT_READY",
|
|
29770
29888
|
NOT_FOUND: "NOT_FOUND",
|
|
29771
29889
|
UNSUPPORTED: "OPERATION_UNSUPPORTED",
|
|
29890
|
+
OUTPUT_PATH_INVALID: "OUTPUT_PATH_INVALID",
|
|
29891
|
+
OUTPUT_EXISTS: "OUTPUT_EXISTS",
|
|
29772
29892
|
REMOTE_REJECTED: "INVALID_INPUT",
|
|
29773
29893
|
NETWORK: "UNAVAILABLE",
|
|
29774
29894
|
CANCELLED: "CANCELLED_BEFORE_COMMIT",
|
|
@@ -30076,6 +30196,8 @@ async function ensureDaemon(options) {
|
|
|
30076
30196
|
} catch (error) {
|
|
30077
30197
|
if (error.code !== "ENOENT") throw error;
|
|
30078
30198
|
}
|
|
30199
|
+
let spawned;
|
|
30200
|
+
let spawnFailed = false;
|
|
30079
30201
|
if (!existingOwner) {
|
|
30080
30202
|
await ensurePrivateDirectory(join15(root, "run"));
|
|
30081
30203
|
const logPath = join15(root, "run", "daemon.log");
|
|
@@ -30092,7 +30214,10 @@ async function ensureDaemon(options) {
|
|
|
30092
30214
|
"daemon",
|
|
30093
30215
|
"run"
|
|
30094
30216
|
], { detached: true, stdio: ["ignore", log.fd, log.fd], windowsHide: true });
|
|
30095
|
-
|
|
30217
|
+
spawned = child;
|
|
30218
|
+
child.on("error", () => {
|
|
30219
|
+
spawnFailed = true;
|
|
30220
|
+
});
|
|
30096
30221
|
child.unref();
|
|
30097
30222
|
} finally {
|
|
30098
30223
|
await log.close();
|
|
@@ -30106,6 +30231,12 @@ async function ensureDaemon(options) {
|
|
|
30106
30231
|
} catch (error) {
|
|
30107
30232
|
if (!(error instanceof DaemonUnavailableError)) throw error;
|
|
30108
30233
|
}
|
|
30234
|
+
if (spawnFailed || spawned && (spawned.exitCode !== null || spawned.signalCode !== null)) {
|
|
30235
|
+
const entries = await readDiagnosticLog(join15(root, "run", "daemon.log"));
|
|
30236
|
+
const failure = entries.findLast((e) => e.pid === spawned?.pid && e.event === "start-failed");
|
|
30237
|
+
if (failure?.error && failure.error.code !== "CLI_UNEXPECTED_FAILURE") throw new CliConfigError(failure.error.code, failure.error.message, failure.error.code === "CONFIG_INVALID" ? 7 : 1);
|
|
30238
|
+
if (spawnFailed) throw new CliConfigError("DAEMON_START_FAILED", "The daemon process could not be spawned. Check the installed Node executable.", 1);
|
|
30239
|
+
}
|
|
30109
30240
|
await delay(80);
|
|
30110
30241
|
}
|
|
30111
30242
|
throw new CliConfigError("DAEMON_START_FAILED", "The daemon did not become ready. Inspect muc daemon logs.", 1);
|
|
@@ -30132,29 +30263,7 @@ async function runDaemonCommand(command2, io) {
|
|
|
30132
30263
|
}
|
|
30133
30264
|
if (command2.action === "run") return runForegroundDaemon(root, io);
|
|
30134
30265
|
if (command2.action === "logs") {
|
|
30135
|
-
|
|
30136
|
-
await assertPrivateRegularOrMissing(path);
|
|
30137
|
-
const handle = await open9(path, "r").catch((e) => {
|
|
30138
|
-
if (e.code === "ENOENT") return void 0;
|
|
30139
|
-
throw e;
|
|
30140
|
-
});
|
|
30141
|
-
if (!handle) return { entries: [] };
|
|
30142
|
-
try {
|
|
30143
|
-
const size = (await handle.stat()).size;
|
|
30144
|
-
const buffer = Buffer.alloc(Math.min(size, 64 * 1024));
|
|
30145
|
-
await handle.read(buffer, 0, buffer.length, Math.max(0, size - buffer.length));
|
|
30146
|
-
const entries = buffer.toString("utf8").split("\n").flatMap((line) => {
|
|
30147
|
-
try {
|
|
30148
|
-
const v = LogSchema.safeParse(JSON.parse(line));
|
|
30149
|
-
return v.success ? [v.data] : [];
|
|
30150
|
-
} catch {
|
|
30151
|
-
return [];
|
|
30152
|
-
}
|
|
30153
|
-
}).slice(-100);
|
|
30154
|
-
return { entries };
|
|
30155
|
-
} finally {
|
|
30156
|
-
await handle.close();
|
|
30157
|
-
}
|
|
30266
|
+
return { entries: await readDiagnosticLog(join15(root, "run", "daemon.log")) };
|
|
30158
30267
|
}
|
|
30159
30268
|
let client;
|
|
30160
30269
|
try {
|
|
@@ -30178,12 +30287,12 @@ async function runDaemonCommand(command2, io) {
|
|
|
30178
30287
|
}
|
|
30179
30288
|
}
|
|
30180
30289
|
async function runForegroundDaemon(root, io) {
|
|
30181
|
-
const log = (event) => io.writeStderr(JSON.stringify({ time: (/* @__PURE__ */ new Date()).toISOString(), event }) + "\n");
|
|
30290
|
+
const log = (event, error) => io.writeStderr(JSON.stringify({ time: (/* @__PURE__ */ new Date()).toISOString(), event, pid: process.pid, ...error === void 0 ? {} : { error: diagnosticError(error) } }) + "\n");
|
|
30182
30291
|
let runtime;
|
|
30183
30292
|
try {
|
|
30184
30293
|
runtime = await NativeV2DaemonRuntime.open({ configRoot: root, onBackgroundError: () => log("background-error") });
|
|
30185
30294
|
} catch (error) {
|
|
30186
|
-
log("start-failed");
|
|
30295
|
+
log("start-failed", error);
|
|
30187
30296
|
throw error;
|
|
30188
30297
|
}
|
|
30189
30298
|
let stop;
|
|
@@ -30204,6 +30313,9 @@ async function runForegroundDaemon(root, io) {
|
|
|
30204
30313
|
await stopped;
|
|
30205
30314
|
log("stopped");
|
|
30206
30315
|
return { stopped: true };
|
|
30316
|
+
} catch (error) {
|
|
30317
|
+
log("start-failed", error);
|
|
30318
|
+
throw error;
|
|
30207
30319
|
} finally {
|
|
30208
30320
|
process.off("SIGTERM", onSignal);
|
|
30209
30321
|
process.off("SIGINT", onSignal);
|
|
@@ -30233,8 +30345,48 @@ var InitSchema = external_exports.object({
|
|
|
30233
30345
|
}).strict();
|
|
30234
30346
|
var LogSchema = external_exports.object({
|
|
30235
30347
|
time: external_exports.string().datetime(),
|
|
30236
|
-
event: external_exports.enum(["started", "stopped", "start-failed", "stop-failed", "background-error"])
|
|
30348
|
+
event: external_exports.enum(["started", "stopped", "start-failed", "stop-failed", "background-error"]),
|
|
30349
|
+
pid: external_exports.number().int().positive().optional(),
|
|
30350
|
+
error: external_exports.object({ code: external_exports.string(), message: external_exports.string(), hint: external_exports.string() }).strict().optional()
|
|
30237
30351
|
}).strict();
|
|
30352
|
+
var diagnostics = {
|
|
30353
|
+
CONFIG_INVALID: { message: "Configuration could not be loaded or migrated.", hint: "Check config.json and credentials.json privately. Preserve the originals and migration backups; do not paste credentials into diagnostics." },
|
|
30354
|
+
LEGACY_DAEMON_RUNNING: { message: "An old daemon is still using this config root.", hint: "Stop the old CLI daemon for this root before retrying." },
|
|
30355
|
+
CLI_UNEXPECTED_FAILURE: { message: "The daemon could not start.", hint: "Check the local configuration, file permissions and installed Node version." }
|
|
30356
|
+
};
|
|
30357
|
+
function diagnosticError(error) {
|
|
30358
|
+
const code = error instanceof CliConfigError && Object.hasOwn(diagnostics, error.code) ? error.code : "CLI_UNEXPECTED_FAILURE";
|
|
30359
|
+
return { code, ...diagnostics[code] };
|
|
30360
|
+
}
|
|
30361
|
+
async function readDiagnosticLog(path) {
|
|
30362
|
+
await assertPrivateRegularOrMissing(path);
|
|
30363
|
+
const handle = await open9(path, "r").catch((e) => {
|
|
30364
|
+
if (e.code === "ENOENT") return void 0;
|
|
30365
|
+
throw e;
|
|
30366
|
+
});
|
|
30367
|
+
if (!handle) return [];
|
|
30368
|
+
try {
|
|
30369
|
+
const size = (await handle.stat()).size;
|
|
30370
|
+
const buffer = Buffer.alloc(Math.min(size, 64 * 1024));
|
|
30371
|
+
await handle.read(buffer, 0, buffer.length, Math.max(0, size - buffer.length));
|
|
30372
|
+
return buffer.toString("utf8").split("\n").flatMap((line) => {
|
|
30373
|
+
try {
|
|
30374
|
+
const parsed = LogSchema.safeParse(JSON.parse(line));
|
|
30375
|
+
if (!parsed.success) return [];
|
|
30376
|
+
const value = parsed.data;
|
|
30377
|
+
if (value.error) {
|
|
30378
|
+
const code = Object.hasOwn(diagnostics, value.error.code) ? value.error.code : "CLI_UNEXPECTED_FAILURE";
|
|
30379
|
+
value.error = { code, ...diagnostics[code] };
|
|
30380
|
+
}
|
|
30381
|
+
return [value];
|
|
30382
|
+
} catch {
|
|
30383
|
+
return [];
|
|
30384
|
+
}
|
|
30385
|
+
}).slice(-100);
|
|
30386
|
+
} finally {
|
|
30387
|
+
await handle.close();
|
|
30388
|
+
}
|
|
30389
|
+
}
|
|
30238
30390
|
|
|
30239
30391
|
// src/ipc/qr-code.ts
|
|
30240
30392
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
@@ -30248,7 +30400,7 @@ function renderTerminalQrCode(value) {
|
|
|
30248
30400
|
}
|
|
30249
30401
|
|
|
30250
30402
|
// src/cli/arguments.ts
|
|
30251
|
-
import { isAbsolute as
|
|
30403
|
+
import { isAbsolute as isAbsolute7 } from "node:path";
|
|
30252
30404
|
|
|
30253
30405
|
// src/domain/mvp-operations.ts
|
|
30254
30406
|
var CLI_OPERATION_DEFINITIONS = Object.freeze(
|
|
@@ -30500,7 +30652,7 @@ function loginSlot(value) {
|
|
|
30500
30652
|
return value;
|
|
30501
30653
|
}
|
|
30502
30654
|
function absolutePath(name, value) {
|
|
30503
|
-
if (!
|
|
30655
|
+
if (!isAbsolute7(value) || value.includes("\0")) {
|
|
30504
30656
|
throw new CliUsageError(`${name} must be an absolute local path.`);
|
|
30505
30657
|
}
|
|
30506
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
|
|
@@ -6,7 +6,7 @@ The default root is `~/.muc`; `MUC_CONFIG_DIR` and `--config-root` select an iso
|
|
|
6
6
|
|
|
7
7
|
- `muc daemon start`: start the background process for this root.
|
|
8
8
|
- `muc daemon status`: inspect without starting it.
|
|
9
|
-
- `muc daemon logs`: read safe diagnostic events, never raw credentials.
|
|
9
|
+
- `muc daemon logs`: read safe diagnostic events with error codes and recovery hints, never raw credentials.
|
|
10
10
|
- `muc daemon stop`: stop this background process; existing credentials and stored messages remain.
|
|
11
11
|
- `muc runtime status --profile-id <id>`: inspect the profile connection.
|
|
12
12
|
- `muc runtime ensure|start|resume|stop --profile-id <id>`: change only the requested profile runtime. Do not use resume to bypass an authentication failure or displacement.
|
|
@@ -18,3 +18,5 @@ User-terminal login: `muc login --account <uid> [--profile-id <id>] [--remember]
|
|
|
18
18
|
The daemon performs accessToken → tokenPwd → remembered-password recovery, with `/emp/get` attempted first. Network errors do not downgrade credentials. Exhausted recovery requires explicit login. Existing profiles preserve their device slot on reauthentication.
|
|
19
19
|
|
|
20
20
|
Use `muc profile logout --profile-id <id>` only when the user requests logout; it clears that profile's session and remembered password.
|
|
21
|
+
|
|
22
|
+
Legacy migration supports flat single-account and activeProfile/profiles multi-account roots. It preserves profile IDs, the selected default, device slots and recovery credentials, with private backups and a resumable journal. Different tenants, a missing default or an undecryptable remembered password fail without overwriting the originals. Preserve remember.salt; do not reinitialize or delete credentials to bypass migration errors.
|