@mu-cabin/muc-cli 0.6.0 → 0.6.1

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
@@ -32,12 +32,18 @@ JSON
32
32
 
33
33
  消费程序保存最后一条成功处理的事件游标,重连时通过 `cursor` 继续。`events.list` 用于持久化事件补取。后台离线期间的历史消息通过 `messages.list/search` 查询,不能把本地事件为空当作离线期间没有消息。
34
34
 
35
+ 启动配置错误会直接返回原因,不再仅等待超时。`daemon logs` 返回安全的错误代码、说明和处理提示,不输出原始异常或凭据。
36
+
35
37
  普通命令按需启动 daemon;`daemon status/logs/stop` 不会启动后台。升级可执行文件前先停止相应配置根的 daemon,更新后再启动。被踢下线或主动停止的 profile 保留停止状态,须明确登录或 `runtime resume` 才能恢复。
36
38
 
37
39
  完整参数见 [REFERENCE.md](REFERENCE.md),agent 操作说明见 [skills/muc/SKILL.md](skills/muc/SKILL.md)。文件消息默认不附加尾注;文字和图片仍遵循原有尾注设置。发送结果不明时查询原 delivery/idempotency key,不自动重发。
38
40
 
39
41
  ## 从旧 CLI 迁移
40
42
 
43
+ 0.6.1 支持单账号平铺配置和 `activeProfile + profiles` 多账号配置;后者保留全部 profile,并将默认账号映射为 `selectedProfileId`。不同租户、缺失默认账号或无法解密的记住密码会拒绝迁移并保留原文件。
44
+
45
+ 迁移后会在启动时登记消息数据库中的账号,确保群信息和消息同步可正常落库。
46
+
41
47
  首次启动会识别当前配置根中的旧版 `config.json` / `credentials.json`,保留设备、插槽、账号、token、记住密码和消息尾注,并生成私有的 `*.v1.backup.json` 备份。迁移只处理所选配置根;旧后台仍运行时会拒绝迁移。旧消息日志、assistant 配置和旧版命令不迁入。
42
48
 
43
49
  旧 `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.0 的命令契约。旧的 `send/history/daemon-*` 命令已移除。
3
+ 本文件描述 0.6.1 的命令契约。旧的 `send/history/daemon-*` 命令已移除。
4
4
 
5
5
  ## 命令结构
6
6
 
package/dist/muc.mjs CHANGED
@@ -11956,26 +11956,29 @@ function deriveKey(root) {
11956
11956
  maxmem: 64 * 1024 * 1024
11957
11957
  });
11958
11958
  }
11959
- function encryptRemembered(plaintext, root) {
11959
+ function encryptRemembered(plaintext, root, profile) {
11960
11960
  if (!plaintext) throw new Error("encryptRemembered: empty plaintext");
11961
11961
  const key = deriveKey(root);
11962
11962
  const iv = randomBytes2(IV_BYTES);
11963
11963
  const cipher = createCipheriv("aes-256-gcm", key, iv);
11964
+ if (profile !== void 0) cipher.setAAD(Buffer.from(profile, "utf8"));
11964
11965
  const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
11965
11966
  const tag = cipher.getAuthTag();
11966
- return `v1.${iv.toString("base64")}.${tag.toString("base64")}.${ct.toString("base64")}`;
11967
+ return `${profile === void 0 ? "v1" : "v2"}.${iv.toString("base64")}.${tag.toString("base64")}.${ct.toString("base64")}`;
11967
11968
  }
11968
- function decryptRemembered(blob, root) {
11969
+ function decryptRemembered(blob, root, profile) {
11969
11970
  try {
11970
11971
  if (typeof blob !== "string") return null;
11971
11972
  const parts = blob.split(".");
11972
- if (parts.length !== 4 || parts[0] !== "v1") return null;
11973
+ if (parts.length !== 4 || !["v1", "v2"].includes(parts[0]) || parts[0] === "v2" && profile === void 0) return null;
11974
+ if (!existsSync(saltFile(root))) return null;
11973
11975
  const iv = Buffer.from(parts[1], "base64");
11974
11976
  const tag = Buffer.from(parts[2], "base64");
11975
11977
  const ct = Buffer.from(parts[3], "base64");
11976
11978
  if (iv.length !== IV_BYTES || tag.length !== TAG_BYTES) return null;
11977
11979
  const key = deriveKey(root);
11978
11980
  const decipher = createDecipheriv("aes-256-gcm", key, iv);
11981
+ if (parts[0] === "v2") decipher.setAAD(Buffer.from(profile, "utf8"));
11979
11982
  decipher.setAuthTag(tag);
11980
11983
  const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
11981
11984
  return pt.toString("utf8");
@@ -12117,6 +12120,103 @@ var LegacyCredentials = external_exports.object({
12117
12120
  rememberedPwd: external_exports.string().optional()
12118
12121
  });
12119
12122
  var Journal = external_exports.object({ config: NativeProfileDocumentSchema, credentials: CredentialDocumentSchema }).strict();
12123
+ var MultiProfile = LegacyConfig.omit({
12124
+ schemaVersion: true,
12125
+ appKey: true,
12126
+ appId: true,
12127
+ signSecretRest: true,
12128
+ signSecretSecure: true,
12129
+ pwdDesKey: true
12130
+ }).extend({
12131
+ account: external_exports.string().min(1),
12132
+ generation: external_exports.string().uuid(),
12133
+ deviceId: LegacyCredentials.shape.deviceId,
12134
+ deviceName: LegacyCredentials.shape.deviceName,
12135
+ identity: external_exports.object({ uid: external_exports.string().optional(), employeeId: external_exports.string().optional(), name: external_exports.string().optional() }).optional()
12136
+ });
12137
+ var MultiConfig = external_exports.object({
12138
+ schemaVersion: external_exports.literal(1),
12139
+ activeProfile: NativeProfileIdSchema,
12140
+ profiles: external_exports.record(NativeProfileIdSchema, MultiProfile)
12141
+ });
12142
+ var MultiCredentials = external_exports.object({ schemaVersion: external_exports.literal(1), profiles: external_exports.record(NativeProfileIdSchema, external_exports.object({
12143
+ tenant: LegacyConfig.pick({ appKey: true, appId: true, signSecretRest: true, signSecretSecure: true, pwdDesKey: true, savedAt: true }).optional(),
12144
+ session: LegacyCredentials.omit({ uid: true, empId: true, name: true, deviceId: true, deviceName: true }).extend({
12145
+ appVersion: external_exports.string().min(1),
12146
+ rememberedPassword: external_exports.string().min(1).optional()
12147
+ }).optional()
12148
+ })) });
12149
+ function convertMulti(root, raw, secrets) {
12150
+ const old = MultiConfig.parse(raw);
12151
+ const creds = MultiCredentials.parse(secrets ?? { schemaVersion: 1, profiles: {} });
12152
+ if (!Object.hasOwn(old.profiles, old.activeProfile)) throw new CliConfigError("CONFIG_INVALID", "Legacy default profile does not exist. Original files are unchanged.");
12153
+ 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.");
12154
+ const profiles = {};
12155
+ const credentials = { schemaVersion: 2, profiles: {} };
12156
+ for (const [id, oldProfile] of Object.entries(old.profiles)) {
12157
+ const profile = createNativeProfile({
12158
+ id,
12159
+ endpoints: {
12160
+ restOrigin: new URL(oldProfile.restHost).origin,
12161
+ imHost: oldProfile.imHost,
12162
+ imPort: oldProfile.imPort,
12163
+ fileHost: oldProfile.fileHost,
12164
+ filePort: oldProfile.filePort
12165
+ },
12166
+ slot: slotFromChatOs(oldProfile.defaultChatOs ?? "iPad"),
12167
+ cliSignature: oldProfile.messageTail === void 0 ? { mode: "builtin" } : oldProfile.messageTail === "" ? { mode: "disabled" } : { mode: "custom", text: oldProfile.messageTail }
12168
+ }, { generation: oldProfile.generation, now: () => oldProfile.savedAt });
12169
+ profile.device = { id: oldProfile.deviceId, name: oldProfile.deviceName };
12170
+ profile.identity = {
12171
+ account: oldProfile.account.toUpperCase(),
12172
+ ...oldProfile.identity?.employeeId ? { employeeId: oldProfile.identity.employeeId } : {},
12173
+ ...oldProfile.identity?.name ? { displayName: oldProfile.identity.name } : {}
12174
+ };
12175
+ profiles[id] = NativeProfileSchema.parse(profile);
12176
+ const secret = creds.profiles[id];
12177
+ if (secret?.tenant) {
12178
+ const t = secret.tenant;
12179
+ const tenant = NativeTenantSecretSchema.parse({
12180
+ schemaVersion: 2,
12181
+ appKey: t.appKey,
12182
+ appId: t.appId,
12183
+ signSecretRest: t.signSecretRest,
12184
+ signSecretSecure: t.signSecretSecure,
12185
+ passwordKey: t.pwdDesKey,
12186
+ savedAtMs: t.savedAt
12187
+ });
12188
+ const { savedAtMs: _, ...identity } = tenant;
12189
+ const { savedAtMs: __, ...existing } = credentials.tenant ?? tenant;
12190
+ 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.");
12191
+ credentials.tenant = tenant;
12192
+ }
12193
+ if (secret?.session) {
12194
+ const s = secret.session;
12195
+ credentials.profiles[profile.storageKey] = {
12196
+ session: NativeSessionSecretSchema.parse({
12197
+ schemaVersion: 2,
12198
+ accessToken: s.accessToken,
12199
+ tokenPassword: s.tokenPwd,
12200
+ restPlatform: s.restPlatform ?? 5,
12201
+ appVersion: s.appVersion,
12202
+ savedAtMs: s.savedAt,
12203
+ accessTokenExpireAtMs: s.accessTokenExpireAt,
12204
+ tokenPasswordExpireAtMs: s.tokenPwdExpireAt
12205
+ }),
12206
+ ...s.rememberedPwd ? { rememberedPwd: s.rememberedPwd } : {}
12207
+ };
12208
+ }
12209
+ }
12210
+ const next = Journal.parse({ config: { schemaVersion: 2, selectedProfileId: old.activeProfile, profiles }, credentials });
12211
+ for (const [id, secret] of Object.entries(creds.profiles)) {
12212
+ if (secret.session?.rememberedPassword) {
12213
+ const password = decryptRemembered(secret.session.rememberedPassword, root, id);
12214
+ 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.");
12215
+ next.credentials.profiles[profiles[id].storageKey].rememberedPwd = encryptRemembered(password, root);
12216
+ }
12217
+ }
12218
+ return next;
12219
+ }
12120
12220
  async function importLegacyStorage(root) {
12121
12221
  try {
12122
12222
  const journalPath = join6(root, ".migration-v2.json");
@@ -12128,47 +12228,8 @@ async function importLegacyStorage(root) {
12128
12228
  await commit(root, Journal.parse(pending));
12129
12229
  return true;
12130
12230
  }
12131
- const legacy = LegacyConfig.parse(config);
12132
12231
  const rawCredentials = await readPrivateJson(join6(root, "credentials.json"));
12133
- const credentials = rawCredentials === void 0 ? void 0 : LegacyCredentials.parse(rawCredentials);
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
- });
12232
+ const next = config && typeof config === "object" && "profiles" in config ? convertMulti(root, config, rawCredentials) : convertSingle(config, rawCredentials);
12172
12233
  await backupOnce(root, "config.v1.backup.json", config);
12173
12234
  if (rawCredentials !== void 0) await backupOnce(root, "credentials.v1.backup.json", rawCredentials);
12174
12235
  await writePrivateJson(root, ".migration-v2.json", next);
@@ -12176,9 +12237,52 @@ async function importLegacyStorage(root) {
12176
12237
  return true;
12177
12238
  } catch (error) {
12178
12239
  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.");
12240
+ throw new CliConfigError("CONFIG_INVALID", "Legacy configuration could not be imported. Original files or migration backups are retained; inspect the local files privately.");
12180
12241
  }
12181
12242
  }
12243
+ function convertSingle(config, rawCredentials) {
12244
+ const legacy = LegacyConfig.parse(config);
12245
+ const credentials = rawCredentials === void 0 ? void 0 : LegacyCredentials.parse(rawCredentials);
12246
+ const profile = createNativeProfile({
12247
+ id: "default",
12248
+ endpoints: { restOrigin: new URL(legacy.restHost).origin, imHost: legacy.imHost, imPort: legacy.imPort, fileHost: legacy.fileHost, filePort: legacy.filePort },
12249
+ slot: slotFromChatOs(legacy.defaultChatOs ?? "iPad"),
12250
+ cliSignature: legacy.messageTail === void 0 ? { mode: "builtin" } : legacy.messageTail === "" ? { mode: "disabled" } : { mode: "custom", text: legacy.messageTail }
12251
+ });
12252
+ if (credentials) {
12253
+ profile.device = { id: credentials.deviceId, name: credentials.deviceName };
12254
+ profile.identity = { account: credentials.uid.toUpperCase(), ...credentials.empId ? { employeeId: credentials.empId } : {}, ...credentials.name ? { displayName: credentials.name } : {} };
12255
+ }
12256
+ const tenant = NativeTenantSecretSchema.parse({
12257
+ schemaVersion: 2,
12258
+ appKey: legacy.appKey,
12259
+ appId: legacy.appId,
12260
+ signSecretRest: legacy.signSecretRest,
12261
+ signSecretSecure: legacy.signSecretSecure,
12262
+ ...legacy.pwdDesKey ? { passwordKey: legacy.pwdDesKey } : {},
12263
+ savedAtMs: legacy.savedAt
12264
+ });
12265
+ const restPlatform = credentials?.restPlatform ?? 5;
12266
+ const next = Journal.parse({
12267
+ config: { schemaVersion: 2, selectedProfileId: profile.id, profiles: { [profile.id]: NativeProfileSchema.parse(profile) } },
12268
+ credentials: { schemaVersion: 2, tenant, profiles: credentials ? {
12269
+ [profile.storageKey]: {
12270
+ session: NativeSessionSecretSchema.parse({
12271
+ schemaVersion: 2,
12272
+ accessToken: credentials.accessToken,
12273
+ tokenPassword: credentials.tokenPwd,
12274
+ restPlatform,
12275
+ appVersion: restPlatform === 1 ? "3.3.0301" : "3.3.1-250925001",
12276
+ savedAtMs: credentials.savedAt,
12277
+ accessTokenExpireAtMs: credentials.accessTokenExpireAt,
12278
+ tokenPasswordExpireAtMs: credentials.tokenPwdExpireAt
12279
+ }),
12280
+ ...credentials.rememberedPwd ? { rememberedPwd: credentials.rememberedPwd } : {}
12281
+ }
12282
+ } : {} }
12283
+ });
12284
+ return next;
12285
+ }
12182
12286
  async function backupOnce(root, name, value) {
12183
12287
  if (await readPrivateJson(join6(root, name)) === void 0) await writePrivateJson(root, name, value);
12184
12288
  }
@@ -12212,7 +12316,7 @@ async function assertLegacyStopped(root) {
12212
12316
  // package.json
12213
12317
  var package_default = {
12214
12318
  name: "@mu-cabin/muc-cli",
12215
- version: "0.6.0",
12319
+ version: "0.6.1",
12216
12320
  description: "Standalone agent-first MUC CLI with an embedded background daemon",
12217
12321
  license: "MIT",
12218
12322
  type: "module",
@@ -22832,6 +22936,14 @@ var NativeDaemonCore = class _NativeDaemonCore {
22832
22936
  state,
22833
22937
  ...delivery === void 0 ? {} : { delivery }
22834
22938
  });
22939
+ for (const profile of await core.profiles.list()) {
22940
+ await state.ensureProfile({
22941
+ profileId: profile.id,
22942
+ ...profile.identity?.account === void 0 ? {} : { accountId: profile.identity.account },
22943
+ ...profile.identity?.displayName === void 0 ? {} : { displayName: profile.identity.displayName },
22944
+ updatedAtMs: profile.updatedAtMs
22945
+ });
22946
+ }
22835
22947
  core.#ownsState = ownsState;
22836
22948
  core.#ownsDelivery = ownsDelivery;
22837
22949
  return core;
@@ -30076,6 +30188,8 @@ async function ensureDaemon(options) {
30076
30188
  } catch (error) {
30077
30189
  if (error.code !== "ENOENT") throw error;
30078
30190
  }
30191
+ let spawned;
30192
+ let spawnFailed = false;
30079
30193
  if (!existingOwner) {
30080
30194
  await ensurePrivateDirectory(join15(root, "run"));
30081
30195
  const logPath = join15(root, "run", "daemon.log");
@@ -30092,7 +30206,10 @@ async function ensureDaemon(options) {
30092
30206
  "daemon",
30093
30207
  "run"
30094
30208
  ], { detached: true, stdio: ["ignore", log.fd, log.fd], windowsHide: true });
30095
- child.on("error", () => void 0);
30209
+ spawned = child;
30210
+ child.on("error", () => {
30211
+ spawnFailed = true;
30212
+ });
30096
30213
  child.unref();
30097
30214
  } finally {
30098
30215
  await log.close();
@@ -30106,6 +30223,12 @@ async function ensureDaemon(options) {
30106
30223
  } catch (error) {
30107
30224
  if (!(error instanceof DaemonUnavailableError)) throw error;
30108
30225
  }
30226
+ if (spawnFailed || spawned && (spawned.exitCode !== null || spawned.signalCode !== null)) {
30227
+ const entries = await readDiagnosticLog(join15(root, "run", "daemon.log"));
30228
+ const failure = entries.findLast((e) => e.pid === spawned?.pid && e.event === "start-failed");
30229
+ 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);
30230
+ if (spawnFailed) throw new CliConfigError("DAEMON_START_FAILED", "The daemon process could not be spawned. Check the installed Node executable.", 1);
30231
+ }
30109
30232
  await delay(80);
30110
30233
  }
30111
30234
  throw new CliConfigError("DAEMON_START_FAILED", "The daemon did not become ready. Inspect muc daemon logs.", 1);
@@ -30132,29 +30255,7 @@ async function runDaemonCommand(command2, io) {
30132
30255
  }
30133
30256
  if (command2.action === "run") return runForegroundDaemon(root, io);
30134
30257
  if (command2.action === "logs") {
30135
- const path = join15(root, "run", "daemon.log");
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
- }
30258
+ return { entries: await readDiagnosticLog(join15(root, "run", "daemon.log")) };
30158
30259
  }
30159
30260
  let client;
30160
30261
  try {
@@ -30178,12 +30279,12 @@ async function runDaemonCommand(command2, io) {
30178
30279
  }
30179
30280
  }
30180
30281
  async function runForegroundDaemon(root, io) {
30181
- const log = (event) => io.writeStderr(JSON.stringify({ time: (/* @__PURE__ */ new Date()).toISOString(), event }) + "\n");
30282
+ 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
30283
  let runtime;
30183
30284
  try {
30184
30285
  runtime = await NativeV2DaemonRuntime.open({ configRoot: root, onBackgroundError: () => log("background-error") });
30185
30286
  } catch (error) {
30186
- log("start-failed");
30287
+ log("start-failed", error);
30187
30288
  throw error;
30188
30289
  }
30189
30290
  let stop;
@@ -30204,6 +30305,9 @@ async function runForegroundDaemon(root, io) {
30204
30305
  await stopped;
30205
30306
  log("stopped");
30206
30307
  return { stopped: true };
30308
+ } catch (error) {
30309
+ log("start-failed", error);
30310
+ throw error;
30207
30311
  } finally {
30208
30312
  process.off("SIGTERM", onSignal);
30209
30313
  process.off("SIGINT", onSignal);
@@ -30233,8 +30337,48 @@ var InitSchema = external_exports.object({
30233
30337
  }).strict();
30234
30338
  var LogSchema = external_exports.object({
30235
30339
  time: external_exports.string().datetime(),
30236
- event: external_exports.enum(["started", "stopped", "start-failed", "stop-failed", "background-error"])
30340
+ event: external_exports.enum(["started", "stopped", "start-failed", "stop-failed", "background-error"]),
30341
+ pid: external_exports.number().int().positive().optional(),
30342
+ error: external_exports.object({ code: external_exports.string(), message: external_exports.string(), hint: external_exports.string() }).strict().optional()
30237
30343
  }).strict();
30344
+ var diagnostics = {
30345
+ 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." },
30346
+ LEGACY_DAEMON_RUNNING: { message: "An old daemon is still using this config root.", hint: "Stop the old CLI daemon for this root before retrying." },
30347
+ CLI_UNEXPECTED_FAILURE: { message: "The daemon could not start.", hint: "Check the local configuration, file permissions and installed Node version." }
30348
+ };
30349
+ function diagnosticError(error) {
30350
+ const code = error instanceof CliConfigError && Object.hasOwn(diagnostics, error.code) ? error.code : "CLI_UNEXPECTED_FAILURE";
30351
+ return { code, ...diagnostics[code] };
30352
+ }
30353
+ async function readDiagnosticLog(path) {
30354
+ await assertPrivateRegularOrMissing(path);
30355
+ const handle = await open9(path, "r").catch((e) => {
30356
+ if (e.code === "ENOENT") return void 0;
30357
+ throw e;
30358
+ });
30359
+ if (!handle) return [];
30360
+ try {
30361
+ const size = (await handle.stat()).size;
30362
+ const buffer = Buffer.alloc(Math.min(size, 64 * 1024));
30363
+ await handle.read(buffer, 0, buffer.length, Math.max(0, size - buffer.length));
30364
+ return buffer.toString("utf8").split("\n").flatMap((line) => {
30365
+ try {
30366
+ const parsed = LogSchema.safeParse(JSON.parse(line));
30367
+ if (!parsed.success) return [];
30368
+ const value = parsed.data;
30369
+ if (value.error) {
30370
+ const code = Object.hasOwn(diagnostics, value.error.code) ? value.error.code : "CLI_UNEXPECTED_FAILURE";
30371
+ value.error = { code, ...diagnostics[code] };
30372
+ }
30373
+ return [value];
30374
+ } catch {
30375
+ return [];
30376
+ }
30377
+ }).slice(-100);
30378
+ } finally {
30379
+ await handle.close();
30380
+ }
30381
+ }
30238
30382
 
30239
30383
  // src/ipc/qr-code.ts
30240
30384
  var import_qrcode = __toESM(require_lib(), 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mu-cabin/muc-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Standalone agent-first MUC CLI with an embedded background daemon",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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.