@kyan-du/agent-wechat-cli 0.13.3 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1199,8 +1199,8 @@ var require_command = __commonJS({
1199
1199
  "../../node_modules/.pnpm/commander@14.0.3/node_modules/commander/lib/command.js"(exports) {
1200
1200
  var EventEmitter = __require("node:events").EventEmitter;
1201
1201
  var childProcess = __require("node:child_process");
1202
- var path5 = __require("node:path");
1203
- var fs5 = __require("node:fs");
1202
+ var path6 = __require("node:path");
1203
+ var fs6 = __require("node:fs");
1204
1204
  var process2 = __require("node:process");
1205
1205
  var { Argument: Argument2, humanReadableArgName } = require_argument();
1206
1206
  var { CommanderError: CommanderError2 } = require_error();
@@ -2194,7 +2194,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2194
2194
  * @param {string} subcommandName
2195
2195
  */
2196
2196
  _checkForMissingExecutable(executableFile, executableDir, subcommandName) {
2197
- if (fs5.existsSync(executableFile)) return;
2197
+ if (fs6.existsSync(executableFile)) return;
2198
2198
  const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory";
2199
2199
  const executableMissing = `'${executableFile}' does not exist
2200
2200
  - if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
@@ -2212,11 +2212,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
2212
2212
  let launchWithNode = false;
2213
2213
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
2214
2214
  function findFile(baseDir, baseName) {
2215
- const localBin = path5.resolve(baseDir, baseName);
2216
- if (fs5.existsSync(localBin)) return localBin;
2217
- if (sourceExt.includes(path5.extname(baseName))) return void 0;
2215
+ const localBin = path6.resolve(baseDir, baseName);
2216
+ if (fs6.existsSync(localBin)) return localBin;
2217
+ if (sourceExt.includes(path6.extname(baseName))) return void 0;
2218
2218
  const foundExt = sourceExt.find(
2219
- (ext) => fs5.existsSync(`${localBin}${ext}`)
2219
+ (ext) => fs6.existsSync(`${localBin}${ext}`)
2220
2220
  );
2221
2221
  if (foundExt) return `${localBin}${foundExt}`;
2222
2222
  return void 0;
@@ -2228,21 +2228,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
2228
2228
  if (this._scriptPath) {
2229
2229
  let resolvedScriptPath;
2230
2230
  try {
2231
- resolvedScriptPath = fs5.realpathSync(this._scriptPath);
2231
+ resolvedScriptPath = fs6.realpathSync(this._scriptPath);
2232
2232
  } catch {
2233
2233
  resolvedScriptPath = this._scriptPath;
2234
2234
  }
2235
- executableDir = path5.resolve(
2236
- path5.dirname(resolvedScriptPath),
2235
+ executableDir = path6.resolve(
2236
+ path6.dirname(resolvedScriptPath),
2237
2237
  executableDir
2238
2238
  );
2239
2239
  }
2240
2240
  if (executableDir) {
2241
2241
  let localFile = findFile(executableDir, executableFile);
2242
2242
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
2243
- const legacyName = path5.basename(
2243
+ const legacyName = path6.basename(
2244
2244
  this._scriptPath,
2245
- path5.extname(this._scriptPath)
2245
+ path6.extname(this._scriptPath)
2246
2246
  );
2247
2247
  if (legacyName !== this._name) {
2248
2248
  localFile = findFile(
@@ -2253,7 +2253,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2253
2253
  }
2254
2254
  executableFile = localFile || executableFile;
2255
2255
  }
2256
- launchWithNode = sourceExt.includes(path5.extname(executableFile));
2256
+ launchWithNode = sourceExt.includes(path6.extname(executableFile));
2257
2257
  let proc;
2258
2258
  if (process2.platform !== "win32") {
2259
2259
  if (launchWithNode) {
@@ -3055,9 +3055,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3055
3055
  * @param {string[]} [aliases]
3056
3056
  * @return {(string[]|Command)}
3057
3057
  */
3058
- aliases(aliases) {
3059
- if (aliases === void 0) return this._aliases;
3060
- aliases.forEach((alias) => this.alias(alias));
3058
+ aliases(aliases2) {
3059
+ if (aliases2 === void 0) return this._aliases;
3060
+ aliases2.forEach((alias) => this.alias(alias));
3061
3061
  return this;
3062
3062
  }
3063
3063
  /**
@@ -3168,7 +3168,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3168
3168
  * @return {Command}
3169
3169
  */
3170
3170
  nameFromFilename(filename) {
3171
- this._name = path5.basename(filename, path5.extname(filename));
3171
+ this._name = path6.basename(filename, path6.extname(filename));
3172
3172
  return this;
3173
3173
  }
3174
3174
  /**
@@ -3182,9 +3182,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3182
3182
  * @param {string} [path]
3183
3183
  * @return {(string|null|Command)}
3184
3184
  */
3185
- executableDir(path6) {
3186
- if (path6 === void 0) return this._executableDir;
3187
- this._executableDir = path6;
3185
+ executableDir(path7) {
3186
+ if (path7 === void 0) return this._executableDir;
3187
+ this._executableDir = path7;
3188
3188
  return this;
3189
3189
  }
3190
3190
  /**
@@ -3482,9 +3482,9 @@ var {
3482
3482
  // src/cli.ts
3483
3483
  import { execFileSync as execFileSync3, spawn } from "node:child_process";
3484
3484
  import { createHash, randomBytes as randomBytes2 } from "node:crypto";
3485
- import fs4 from "node:fs";
3485
+ import fs5 from "node:fs";
3486
3486
  import os2 from "node:os";
3487
- import path4 from "node:path";
3487
+ import path5 from "node:path";
3488
3488
  import readline from "node:readline/promises";
3489
3489
  import { fileURLToPath } from "node:url";
3490
3490
  import qrTerminal from "qrcode-terminal";
@@ -3967,8 +3967,8 @@ function getErrorMap() {
3967
3967
 
3968
3968
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
3969
3969
  var makeIssue = (params) => {
3970
- const { data, path: path5, errorMaps, issueData } = params;
3971
- const fullPath = [...path5, ...issueData.path || []];
3970
+ const { data, path: path6, errorMaps, issueData } = params;
3971
+ const fullPath = [...path6, ...issueData.path || []];
3972
3972
  const fullIssue = {
3973
3973
  ...issueData,
3974
3974
  path: fullPath
@@ -4084,11 +4084,11 @@ var errorUtil;
4084
4084
 
4085
4085
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
4086
4086
  var ParseInputLazyPath = class {
4087
- constructor(parent, value, path5, key) {
4087
+ constructor(parent, value, path6, key) {
4088
4088
  this._cachedPath = [];
4089
4089
  this.parent = parent;
4090
4090
  this.data = value;
4091
- this._path = path5;
4091
+ this._path = path6;
4092
4092
  this._key = key;
4093
4093
  }
4094
4094
  get path() {
@@ -7585,6 +7585,23 @@ var chatSchema = external_exports.object({
7585
7585
  unreadCount: external_exports.number().int().nonnegative(),
7586
7586
  isGroup: external_exports.boolean()
7587
7587
  });
7588
+ var groupMemberSchema = external_exports.object({
7589
+ memberId: external_exports.string().min(1),
7590
+ displayName: external_exports.string().min(1),
7591
+ groupAlias: external_exports.string().optional(),
7592
+ nickName: external_exports.string().optional()
7593
+ });
7594
+ var groupMembersPageSchema = external_exports.object({
7595
+ schemaVersion: external_exports.literal(1),
7596
+ items: external_exports.array(groupMemberSchema),
7597
+ nextCursor: external_exports.string().nullable().optional(),
7598
+ errorCode: external_exports.string().optional()
7599
+ });
7600
+ var listGroupMembersParamsSchema = external_exports.object({
7601
+ groupId: external_exports.string().endsWith("@chatroom"),
7602
+ limit: external_exports.number().int().positive().max(100).optional().default(50),
7603
+ cursor: external_exports.string().optional()
7604
+ });
7588
7605
  var listChatsParamsSchema = external_exports.object({
7589
7606
  limit: external_exports.number().int().positive().max(100).optional().default(50),
7590
7607
  offset: external_exports.number().int().nonnegative().optional().default(0)
@@ -7611,6 +7628,22 @@ var openChatResultSchema = external_exports.object({
7611
7628
  error: external_exports.string().optional()
7612
7629
  });
7613
7630
  var idempotencyKeySchema = external_exports.string().min(1).max(128).regex(/^[A-Za-z0-9._:-]+$/);
7631
+ var forwardedMessageNodeSchema = external_exports.lazy(() => external_exports.object({
7632
+ sender: external_exports.string().optional(),
7633
+ senderId: external_exports.string().optional(),
7634
+ timestamp: external_exports.string().optional(),
7635
+ text: external_exports.string().optional(),
7636
+ messageType: external_exports.number().int().optional(),
7637
+ media: external_exports.string().optional(),
7638
+ children: external_exports.array(forwardedMessageNodeSchema),
7639
+ truncated: external_exports.boolean()
7640
+ }));
7641
+ var forwardedMessageTreeSchema = external_exports.object({
7642
+ schemaVersion: external_exports.number().int(),
7643
+ title: external_exports.string().optional(),
7644
+ nodes: external_exports.array(forwardedMessageNodeSchema),
7645
+ truncated: external_exports.boolean()
7646
+ });
7614
7647
  var messageSchema = external_exports.object({
7615
7648
  localId: external_exports.number().int(),
7616
7649
  serverId: external_exports.number(),
@@ -7619,7 +7652,26 @@ var messageSchema = external_exports.object({
7619
7652
  senderName: external_exports.string().optional(),
7620
7653
  type: external_exports.number().int(),
7621
7654
  content: external_exports.string(),
7622
- timestamp: external_exports.string()
7655
+ timestamp: external_exports.string(),
7656
+ forwarded: forwardedMessageTreeSchema.optional()
7657
+ });
7658
+ var mediaReferenceSchema = external_exports.object({
7659
+ localId: external_exports.number().int(),
7660
+ url: external_exports.string().min(1)
7661
+ });
7662
+ var syncReadStateSchema = external_exports.object({
7663
+ unreadCount: external_exports.number().int().nonnegative(),
7664
+ observedAt: external_exports.string().min(1)
7665
+ });
7666
+ var chatSyncPageSchema = external_exports.object({
7667
+ schemaVersion: external_exports.literal(1),
7668
+ chat: chatSchema.optional(),
7669
+ items: external_exports.array(messageSchema),
7670
+ nextCursor: external_exports.string().optional().nullable(),
7671
+ syncToken: external_exports.string(),
7672
+ readState: syncReadStateSchema,
7673
+ media: external_exports.array(mediaReferenceSchema),
7674
+ errorCode: external_exports.string().optional()
7623
7675
  });
7624
7676
  var listMessagesParamsSchema = external_exports.object({
7625
7677
  chatId: external_exports.string().min(1),
@@ -7645,6 +7697,31 @@ var sendParamsSchema = external_exports.object({
7645
7697
  source: external_exports.string().min(1).max(64).regex(/^[A-Za-z0-9._:-]+$/).optional(),
7646
7698
  similarityConfirmed: external_exports.boolean().optional()
7647
7699
  });
7700
+ var deliveryStateSchema = external_exports.enum(["queued", "composing", "submitted", "observed_in_chat", "confirmed", "uncertain", "failed"]);
7701
+ var deliveryCauseSchema = external_exports.enum(["send_accepted", "target_sender_and_payload_match", "observation_confirmed", "target_mismatch", "sender_unverified", "payload_mismatch", "pre_commit_failure", "post_commit_uncertain"]);
7702
+ var deliveryTransitionSchema = external_exports.object({ from: deliveryStateSchema, to: deliveryStateSchema, at: external_exports.string().datetime(), reason: deliveryCauseSchema });
7703
+ var deliveryObservationSchema = external_exports.object({
7704
+ chatId: external_exports.string().trim().min(1).max(512),
7705
+ localId: external_exports.number().int().nonnegative().safe(),
7706
+ serverId: external_exports.number().int().nonnegative().safe(),
7707
+ timestamp: external_exports.string().datetime(),
7708
+ type: external_exports.number().int().nonnegative().safe(),
7709
+ sender: external_exports.string().trim().min(1).max(512),
7710
+ content: external_exports.string().max(1e6)
7711
+ }).strict();
7712
+ var deliveryAttemptSchema = external_exports.object({
7713
+ schemaVersion: external_exports.literal(1),
7714
+ idempotencyKey: idempotencyKeySchema.optional(),
7715
+ senderId: external_exports.string().trim().min(1).max(512),
7716
+ targetChatId: external_exports.string().trim().min(1).max(512),
7717
+ payloadDigest: external_exports.string().regex(/^[a-f0-9]{64}$/),
7718
+ state: deliveryStateSchema,
7719
+ commitAttempted: external_exports.boolean(),
7720
+ createdAt: external_exports.string().datetime(),
7721
+ updatedAt: external_exports.string().datetime(),
7722
+ observedLocalId: external_exports.number().int().nonnegative().optional(),
7723
+ transitions: external_exports.array(deliveryTransitionSchema).max(32)
7724
+ });
7648
7725
  var sendResultSchema = external_exports.object({
7649
7726
  success: external_exports.boolean(),
7650
7727
  messageId: external_exports.string().optional(),
@@ -7657,11 +7734,13 @@ var getMediaParamsSchema = external_exports.object({
7657
7734
  localId: external_exports.number().int()
7658
7735
  });
7659
7736
  var mediaResultSchema = external_exports.object({
7660
- type: external_exports.enum(["image", "emoji", "voice", "video", "unsupported"]),
7737
+ type: external_exports.enum(["image", "emoji", "file", "voice", "video", "pending", "unsupported"]),
7661
7738
  data: external_exports.string().optional(),
7662
7739
  url: external_exports.string().optional(),
7663
7740
  format: external_exports.string(),
7664
- filename: external_exports.string()
7741
+ filename: external_exports.string(),
7742
+ source: external_exports.enum(["original", "thumbnail"]).optional(),
7743
+ errorCode: external_exports.string().optional()
7665
7744
  });
7666
7745
  var agentConfigSchema = external_exports.object({
7667
7746
  maxTurns: external_exports.number().int().positive().default(30),
@@ -7726,16 +7805,16 @@ var WeChatClient = class {
7726
7805
  return this.base;
7727
7806
  }
7728
7807
  // ---- internal helpers ----
7729
- async get(path5) {
7730
- const res = await fetch(`${this.base}${path5}`, {
7808
+ async get(path6) {
7809
+ const res = await fetch(`${this.base}${path6}`, {
7731
7810
  headers: this.headers
7732
7811
  });
7733
7812
  if (!res.ok)
7734
7813
  await throwHttpError(res);
7735
7814
  return res.json();
7736
7815
  }
7737
- async post(path5, body) {
7738
- const res = await fetch(`${this.base}${path5}`, {
7816
+ async post(path6, body) {
7817
+ const res = await fetch(`${this.base}${path6}`, {
7739
7818
  method: "POST",
7740
7819
  headers: this.headers,
7741
7820
  body: body != null ? JSON.stringify(body) : void 0
@@ -7786,6 +7865,12 @@ var WeChatClient = class {
7786
7865
  async openChat(chatId, clearUnreads) {
7787
7866
  return this.post(`/api/chats/${encodeURIComponent(chatId)}/open${qs({ clearUnreads })}`);
7788
7867
  }
7868
+ async listGroupMembersPage(groupId, limit, cursor) {
7869
+ const page = groupMembersPageSchema.parse(await this.get(`/api/groups/${encodeURIComponent(groupId)}/members${qs({ limit, cursor })}`));
7870
+ if (page.errorCode)
7871
+ throw new WeChatHttpError(400, "Bad Request", page.errorCode, page.errorCode);
7872
+ return { ...page, nextCursor: page.nextCursor ?? void 0 };
7873
+ }
7789
7874
  // ---- Contacts ----
7790
7875
  async listContactsPage(limit, cursor) {
7791
7876
  const page = await this.get(`/api/contacts${qs({ limit, cursor })}`);
@@ -7809,6 +7894,17 @@ var WeChatClient = class {
7809
7894
  async listMessages(chatId, limit) {
7810
7895
  return (await this.listMessagesPage(chatId, limit)).items;
7811
7896
  }
7897
+ async syncChat(chatId, options = {}) {
7898
+ const raw = await this.get(`/api/sync/${encodeURIComponent(chatId)}${qs({ limit: options.limit, cursor: options.cursor, since: options.since, from: options.from, to: options.to })}`);
7899
+ if (raw && typeof raw === "object" && typeof raw.errorCode === "string") {
7900
+ const errorCode = raw.errorCode;
7901
+ throw new WeChatHttpError(400, "Bad Request", errorCode, errorCode);
7902
+ }
7903
+ const page = chatSyncPageSchema.parse(raw);
7904
+ if (page.errorCode)
7905
+ throw new WeChatHttpError(400, "Bad Request", page.errorCode, page.errorCode);
7906
+ return { ...page, nextCursor: page.nextCursor ?? void 0 };
7907
+ }
7812
7908
  async getMedia(chatId, localId) {
7813
7909
  return this.get(`/api/messages/${encodeURIComponent(chatId)}/media/${localId}`);
7814
7910
  }
@@ -8178,9 +8274,8 @@ function buildDockerRunArgs(identity, opts) {
8178
8274
  "-e",
8179
8275
  `AGENT_WECHAT_MAC=${identity.mac}`
8180
8276
  ];
8181
- if (opts.proxy) {
8182
- args.push("-e", `PROXY=${opts.proxy}`);
8183
- }
8277
+ if (opts.proxy) args.push("-e", `PROXY=${opts.proxy}`);
8278
+ for (const [key, value] of Object.entries(opts.outbound ?? {})) args.push("-e", `${key}=${value}`);
8184
8279
  args.push(opts.image);
8185
8280
  return args;
8186
8281
  }
@@ -8210,17 +8305,18 @@ function buildCliSendParams(options) {
8210
8305
  var FORK_IMAGE = "ghcr.io/kyan-du/agent-wechat";
8211
8306
  var VERSION_TAG = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/;
8212
8307
  var DIGEST = /^sha256:[0-9a-f]{64}$/;
8308
+ var COMMIT_TAG = /^[a-f0-9]{7}$/;
8213
8309
  function validatePublishedImageReference(reference) {
8214
8310
  const tagPrefix = `${FORK_IMAGE}:`;
8215
8311
  const digestPrefix = `${FORK_IMAGE}@`;
8216
- if (reference.startsWith(tagPrefix) && VERSION_TAG.test(reference.slice(tagPrefix.length))) {
8312
+ if (reference.startsWith(tagPrefix) && (VERSION_TAG.test(reference.slice(tagPrefix.length)) || COMMIT_TAG.test(reference.slice(tagPrefix.length)))) {
8217
8313
  return reference;
8218
8314
  }
8219
8315
  if (reference.startsWith(digestPrefix) && DIGEST.test(reference.slice(digestPrefix.length))) {
8220
8316
  return reference;
8221
8317
  }
8222
8318
  throw new Error(
8223
- `Invalid image reference. Use ${FORK_IMAGE}:<semver> or ${FORK_IMAGE}@sha256:<64 lowercase hex characters>.`
8319
+ `Invalid image reference. Use ${FORK_IMAGE}:<semver>, ${FORK_IMAGE}:<7-character commit SHA>, or ${FORK_IMAGE}@sha256:<64 lowercase hex characters>.`
8224
8320
  );
8225
8321
  }
8226
8322
  function localBuildImage(platform = process.platform, arch = process.arch) {
@@ -8326,8 +8422,8 @@ function assertSafePurgePath(target, root) {
8326
8422
 
8327
8423
  // src/lifecycle.ts
8328
8424
  import { execFileSync } from "node:child_process";
8329
- import fs3 from "node:fs";
8330
- import path3 from "node:path";
8425
+ import fs4 from "node:fs";
8426
+ import path4 from "node:path";
8331
8427
 
8332
8428
  // src/exit-contract.ts
8333
8429
  var EXIT = {
@@ -8369,47 +8465,910 @@ function failure(error) {
8369
8465
  };
8370
8466
  }
8371
8467
 
8372
- // src/lifecycle-policy.ts
8373
- var INSTANCE_LABEL = "dev.visionclaw.agent-wechat.instance";
8374
- var IMAGE_LABEL = "dev.visionclaw.agent-wechat.image";
8375
- var VOLUME_ROLE_LABEL = "dev.visionclaw.agent-wechat.volume-role";
8376
- function exactEnv(env, key) {
8377
- const values = (env ?? []).filter((entry) => entry.startsWith(`${key}=`)).map((entry) => entry.slice(key.length + 1));
8378
- return values.length === 1 ? values[0] : void 0;
8468
+ // src/outbound-config.ts
8469
+ import fs3 from "node:fs";
8470
+ import path3 from "node:path";
8471
+
8472
+ // ../../node_modules/.pnpm/smol-toml@1.6.0/node_modules/smol-toml/dist/error.js
8473
+ function getLineColFromPtr(string, ptr) {
8474
+ let lines = string.slice(0, ptr).split(/\r\n|\n|\r/g);
8475
+ return [lines.length, lines.pop().length + 1];
8476
+ }
8477
+ function makeCodeBlock(string, line, column) {
8478
+ let lines = string.split(/\r\n|\n|\r/g);
8479
+ let codeblock = "";
8480
+ let numberLen = (Math.log10(line + 1) | 0) + 1;
8481
+ for (let i = line - 1; i <= line + 1; i++) {
8482
+ let l = lines[i - 1];
8483
+ if (!l)
8484
+ continue;
8485
+ codeblock += i.toString().padEnd(numberLen, " ");
8486
+ codeblock += ": ";
8487
+ codeblock += l;
8488
+ codeblock += "\n";
8489
+ if (i === line) {
8490
+ codeblock += " ".repeat(numberLen + column + 2);
8491
+ codeblock += "^\n";
8492
+ }
8493
+ }
8494
+ return codeblock;
8495
+ }
8496
+ var TomlError = class extends Error {
8497
+ line;
8498
+ column;
8499
+ codeblock;
8500
+ constructor(message, options) {
8501
+ const [line, column] = getLineColFromPtr(options.toml, options.ptr);
8502
+ const codeblock = makeCodeBlock(options.toml, line, column);
8503
+ super(`Invalid TOML document: ${message}
8504
+
8505
+ ${codeblock}`, options);
8506
+ this.line = line;
8507
+ this.column = column;
8508
+ this.codeblock = codeblock;
8509
+ }
8510
+ };
8511
+
8512
+ // ../../node_modules/.pnpm/smol-toml@1.6.0/node_modules/smol-toml/dist/util.js
8513
+ function isEscaped(str, ptr) {
8514
+ let i = 0;
8515
+ while (str[ptr - ++i] === "\\")
8516
+ ;
8517
+ return --i && i % 2;
8518
+ }
8519
+ function indexOfNewline(str, start = 0, end = str.length) {
8520
+ let idx = str.indexOf("\n", start);
8521
+ if (str[idx - 1] === "\r")
8522
+ idx--;
8523
+ return idx <= end ? idx : -1;
8524
+ }
8525
+ function skipComment(str, ptr) {
8526
+ for (let i = ptr; i < str.length; i++) {
8527
+ let c = str[i];
8528
+ if (c === "\n")
8529
+ return i;
8530
+ if (c === "\r" && str[i + 1] === "\n")
8531
+ return i + 1;
8532
+ if (c < " " && c !== " " || c === "\x7F") {
8533
+ throw new TomlError("control characters are not allowed in comments", {
8534
+ toml: str,
8535
+ ptr
8536
+ });
8537
+ }
8538
+ }
8539
+ return str.length;
8540
+ }
8541
+ function skipVoid(str, ptr, banNewLines, banComments) {
8542
+ let c;
8543
+ while ((c = str[ptr]) === " " || c === " " || !banNewLines && (c === "\n" || c === "\r" && str[ptr + 1] === "\n"))
8544
+ ptr++;
8545
+ return banComments || c !== "#" ? ptr : skipVoid(str, skipComment(str, ptr), banNewLines);
8546
+ }
8547
+ function skipUntil(str, ptr, sep, end, banNewLines = false) {
8548
+ if (!end) {
8549
+ ptr = indexOfNewline(str, ptr);
8550
+ return ptr < 0 ? str.length : ptr;
8551
+ }
8552
+ for (let i = ptr; i < str.length; i++) {
8553
+ let c = str[i];
8554
+ if (c === "#") {
8555
+ i = indexOfNewline(str, i);
8556
+ } else if (c === sep) {
8557
+ return i + 1;
8558
+ } else if (c === end || banNewLines && (c === "\n" || c === "\r" && str[i + 1] === "\n")) {
8559
+ return i;
8560
+ }
8561
+ }
8562
+ throw new TomlError("cannot find end of structure", {
8563
+ toml: str,
8564
+ ptr
8565
+ });
8566
+ }
8567
+ function getStringEnd(str, seek) {
8568
+ let first = str[seek];
8569
+ let target = first === str[seek + 1] && str[seek + 1] === str[seek + 2] ? str.slice(seek, seek + 3) : first;
8570
+ seek += target.length - 1;
8571
+ do
8572
+ seek = str.indexOf(target, ++seek);
8573
+ while (seek > -1 && first !== "'" && isEscaped(str, seek));
8574
+ if (seek > -1) {
8575
+ seek += target.length;
8576
+ if (target.length > 1) {
8577
+ if (str[seek] === first)
8578
+ seek++;
8579
+ if (str[seek] === first)
8580
+ seek++;
8581
+ }
8582
+ }
8583
+ return seek;
8584
+ }
8585
+
8586
+ // ../../node_modules/.pnpm/smol-toml@1.6.0/node_modules/smol-toml/dist/date.js
8587
+ var DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[T ]?(?:(\d{2}):\d{2}(?::\d{2}(?:\.\d+)?)?)?(Z|[-+]\d{2}:\d{2})?$/i;
8588
+ var TomlDate = class _TomlDate extends Date {
8589
+ #hasDate = false;
8590
+ #hasTime = false;
8591
+ #offset = null;
8592
+ constructor(date) {
8593
+ let hasDate = true;
8594
+ let hasTime = true;
8595
+ let offset = "Z";
8596
+ if (typeof date === "string") {
8597
+ let match = date.match(DATE_TIME_RE);
8598
+ if (match) {
8599
+ if (!match[1]) {
8600
+ hasDate = false;
8601
+ date = `0000-01-01T${date}`;
8602
+ }
8603
+ hasTime = !!match[2];
8604
+ hasTime && date[10] === " " && (date = date.replace(" ", "T"));
8605
+ if (match[2] && +match[2] > 23) {
8606
+ date = "";
8607
+ } else {
8608
+ offset = match[3] || null;
8609
+ date = date.toUpperCase();
8610
+ if (!offset && hasTime)
8611
+ date += "Z";
8612
+ }
8613
+ } else {
8614
+ date = "";
8615
+ }
8616
+ }
8617
+ super(date);
8618
+ if (!isNaN(this.getTime())) {
8619
+ this.#hasDate = hasDate;
8620
+ this.#hasTime = hasTime;
8621
+ this.#offset = offset;
8622
+ }
8623
+ }
8624
+ isDateTime() {
8625
+ return this.#hasDate && this.#hasTime;
8626
+ }
8627
+ isLocal() {
8628
+ return !this.#hasDate || !this.#hasTime || !this.#offset;
8629
+ }
8630
+ isDate() {
8631
+ return this.#hasDate && !this.#hasTime;
8632
+ }
8633
+ isTime() {
8634
+ return this.#hasTime && !this.#hasDate;
8635
+ }
8636
+ isValid() {
8637
+ return this.#hasDate || this.#hasTime;
8638
+ }
8639
+ toISOString() {
8640
+ let iso = super.toISOString();
8641
+ if (this.isDate())
8642
+ return iso.slice(0, 10);
8643
+ if (this.isTime())
8644
+ return iso.slice(11, 23);
8645
+ if (this.#offset === null)
8646
+ return iso.slice(0, -1);
8647
+ if (this.#offset === "Z")
8648
+ return iso;
8649
+ let offset = +this.#offset.slice(1, 3) * 60 + +this.#offset.slice(4, 6);
8650
+ offset = this.#offset[0] === "-" ? offset : -offset;
8651
+ let offsetDate = new Date(this.getTime() - offset * 6e4);
8652
+ return offsetDate.toISOString().slice(0, -1) + this.#offset;
8653
+ }
8654
+ static wrapAsOffsetDateTime(jsDate, offset = "Z") {
8655
+ let date = new _TomlDate(jsDate);
8656
+ date.#offset = offset;
8657
+ return date;
8658
+ }
8659
+ static wrapAsLocalDateTime(jsDate) {
8660
+ let date = new _TomlDate(jsDate);
8661
+ date.#offset = null;
8662
+ return date;
8663
+ }
8664
+ static wrapAsLocalDate(jsDate) {
8665
+ let date = new _TomlDate(jsDate);
8666
+ date.#hasTime = false;
8667
+ date.#offset = null;
8668
+ return date;
8669
+ }
8670
+ static wrapAsLocalTime(jsDate) {
8671
+ let date = new _TomlDate(jsDate);
8672
+ date.#hasDate = false;
8673
+ date.#offset = null;
8674
+ return date;
8675
+ }
8676
+ };
8677
+
8678
+ // ../../node_modules/.pnpm/smol-toml@1.6.0/node_modules/smol-toml/dist/primitive.js
8679
+ var INT_REGEX = /^((0x[0-9a-fA-F](_?[0-9a-fA-F])*)|(([+-]|0[ob])?\d(_?\d)*))$/;
8680
+ var FLOAT_REGEX = /^[+-]?\d(_?\d)*(\.\d(_?\d)*)?([eE][+-]?\d(_?\d)*)?$/;
8681
+ var LEADING_ZERO = /^[+-]?0[0-9_]/;
8682
+ var ESCAPE_REGEX = /^[0-9a-f]{2,8}$/i;
8683
+ var ESC_MAP = {
8684
+ b: "\b",
8685
+ t: " ",
8686
+ n: "\n",
8687
+ f: "\f",
8688
+ r: "\r",
8689
+ e: "\x1B",
8690
+ '"': '"',
8691
+ "\\": "\\"
8692
+ };
8693
+ function parseString(str, ptr = 0, endPtr = str.length) {
8694
+ let isLiteral = str[ptr] === "'";
8695
+ let isMultiline = str[ptr++] === str[ptr] && str[ptr] === str[ptr + 1];
8696
+ if (isMultiline) {
8697
+ endPtr -= 2;
8698
+ if (str[ptr += 2] === "\r")
8699
+ ptr++;
8700
+ if (str[ptr] === "\n")
8701
+ ptr++;
8702
+ }
8703
+ let tmp = 0;
8704
+ let isEscape;
8705
+ let parsed = "";
8706
+ let sliceStart = ptr;
8707
+ while (ptr < endPtr - 1) {
8708
+ let c = str[ptr++];
8709
+ if (c === "\n" || c === "\r" && str[ptr] === "\n") {
8710
+ if (!isMultiline) {
8711
+ throw new TomlError("newlines are not allowed in strings", {
8712
+ toml: str,
8713
+ ptr: ptr - 1
8714
+ });
8715
+ }
8716
+ } else if (c < " " && c !== " " || c === "\x7F") {
8717
+ throw new TomlError("control characters are not allowed in strings", {
8718
+ toml: str,
8719
+ ptr: ptr - 1
8720
+ });
8721
+ }
8722
+ if (isEscape) {
8723
+ isEscape = false;
8724
+ if (c === "x" || c === "u" || c === "U") {
8725
+ let code = str.slice(ptr, ptr += c === "x" ? 2 : c === "u" ? 4 : 8);
8726
+ if (!ESCAPE_REGEX.test(code)) {
8727
+ throw new TomlError("invalid unicode escape", {
8728
+ toml: str,
8729
+ ptr: tmp
8730
+ });
8731
+ }
8732
+ try {
8733
+ parsed += String.fromCodePoint(parseInt(code, 16));
8734
+ } catch {
8735
+ throw new TomlError("invalid unicode escape", {
8736
+ toml: str,
8737
+ ptr: tmp
8738
+ });
8739
+ }
8740
+ } else if (isMultiline && (c === "\n" || c === " " || c === " " || c === "\r")) {
8741
+ ptr = skipVoid(str, ptr - 1, true);
8742
+ if (str[ptr] !== "\n" && str[ptr] !== "\r") {
8743
+ throw new TomlError("invalid escape: only line-ending whitespace may be escaped", {
8744
+ toml: str,
8745
+ ptr: tmp
8746
+ });
8747
+ }
8748
+ ptr = skipVoid(str, ptr);
8749
+ } else if (c in ESC_MAP) {
8750
+ parsed += ESC_MAP[c];
8751
+ } else {
8752
+ throw new TomlError("unrecognized escape sequence", {
8753
+ toml: str,
8754
+ ptr: tmp
8755
+ });
8756
+ }
8757
+ sliceStart = ptr;
8758
+ } else if (!isLiteral && c === "\\") {
8759
+ tmp = ptr - 1;
8760
+ isEscape = true;
8761
+ parsed += str.slice(sliceStart, tmp);
8762
+ }
8763
+ }
8764
+ return parsed + str.slice(sliceStart, endPtr - 1);
8765
+ }
8766
+ function parseValue(value, toml, ptr, integersAsBigInt) {
8767
+ if (value === "true")
8768
+ return true;
8769
+ if (value === "false")
8770
+ return false;
8771
+ if (value === "-inf")
8772
+ return -Infinity;
8773
+ if (value === "inf" || value === "+inf")
8774
+ return Infinity;
8775
+ if (value === "nan" || value === "+nan" || value === "-nan")
8776
+ return NaN;
8777
+ if (value === "-0")
8778
+ return integersAsBigInt ? 0n : 0;
8779
+ let isInt = INT_REGEX.test(value);
8780
+ if (isInt || FLOAT_REGEX.test(value)) {
8781
+ if (LEADING_ZERO.test(value)) {
8782
+ throw new TomlError("leading zeroes are not allowed", {
8783
+ toml,
8784
+ ptr
8785
+ });
8786
+ }
8787
+ value = value.replace(/_/g, "");
8788
+ let numeric = +value;
8789
+ if (isNaN(numeric)) {
8790
+ throw new TomlError("invalid number", {
8791
+ toml,
8792
+ ptr
8793
+ });
8794
+ }
8795
+ if (isInt) {
8796
+ if ((isInt = !Number.isSafeInteger(numeric)) && !integersAsBigInt) {
8797
+ throw new TomlError("integer value cannot be represented losslessly", {
8798
+ toml,
8799
+ ptr
8800
+ });
8801
+ }
8802
+ if (isInt || integersAsBigInt === true)
8803
+ numeric = BigInt(value);
8804
+ }
8805
+ return numeric;
8806
+ }
8807
+ const date = new TomlDate(value);
8808
+ if (!date.isValid()) {
8809
+ throw new TomlError("invalid value", {
8810
+ toml,
8811
+ ptr
8812
+ });
8813
+ }
8814
+ return date;
8815
+ }
8816
+
8817
+ // ../../node_modules/.pnpm/smol-toml@1.6.0/node_modules/smol-toml/dist/extract.js
8818
+ function sliceAndTrimEndOf(str, startPtr, endPtr) {
8819
+ let value = str.slice(startPtr, endPtr);
8820
+ let commentIdx = value.indexOf("#");
8821
+ if (commentIdx > -1) {
8822
+ skipComment(str, commentIdx);
8823
+ value = value.slice(0, commentIdx);
8824
+ }
8825
+ return [value.trimEnd(), commentIdx];
8826
+ }
8827
+ function extractValue(str, ptr, end, depth, integersAsBigInt) {
8828
+ if (depth === 0) {
8829
+ throw new TomlError("document contains excessively nested structures. aborting.", {
8830
+ toml: str,
8831
+ ptr
8832
+ });
8833
+ }
8834
+ let c = str[ptr];
8835
+ if (c === "[" || c === "{") {
8836
+ let [value, endPtr2] = c === "[" ? parseArray(str, ptr, depth, integersAsBigInt) : parseInlineTable(str, ptr, depth, integersAsBigInt);
8837
+ if (end) {
8838
+ endPtr2 = skipVoid(str, endPtr2);
8839
+ if (str[endPtr2] === ",")
8840
+ endPtr2++;
8841
+ else if (str[endPtr2] !== end) {
8842
+ throw new TomlError("expected comma or end of structure", {
8843
+ toml: str,
8844
+ ptr: endPtr2
8845
+ });
8846
+ }
8847
+ }
8848
+ return [value, endPtr2];
8849
+ }
8850
+ let endPtr;
8851
+ if (c === '"' || c === "'") {
8852
+ endPtr = getStringEnd(str, ptr);
8853
+ let parsed = parseString(str, ptr, endPtr);
8854
+ if (end) {
8855
+ endPtr = skipVoid(str, endPtr);
8856
+ if (str[endPtr] && str[endPtr] !== "," && str[endPtr] !== end && str[endPtr] !== "\n" && str[endPtr] !== "\r") {
8857
+ throw new TomlError("unexpected character encountered", {
8858
+ toml: str,
8859
+ ptr: endPtr
8860
+ });
8861
+ }
8862
+ endPtr += +(str[endPtr] === ",");
8863
+ }
8864
+ return [parsed, endPtr];
8865
+ }
8866
+ endPtr = skipUntil(str, ptr, ",", end);
8867
+ let slice = sliceAndTrimEndOf(str, ptr, endPtr - +(str[endPtr - 1] === ","));
8868
+ if (!slice[0]) {
8869
+ throw new TomlError("incomplete key-value declaration: no value specified", {
8870
+ toml: str,
8871
+ ptr
8872
+ });
8873
+ }
8874
+ if (end && slice[1] > -1) {
8875
+ endPtr = skipVoid(str, ptr + slice[1]);
8876
+ endPtr += +(str[endPtr] === ",");
8877
+ }
8878
+ return [
8879
+ parseValue(slice[0], str, ptr, integersAsBigInt),
8880
+ endPtr
8881
+ ];
8882
+ }
8883
+
8884
+ // ../../node_modules/.pnpm/smol-toml@1.6.0/node_modules/smol-toml/dist/struct.js
8885
+ var KEY_PART_RE = /^[a-zA-Z0-9-_]+[ \t]*$/;
8886
+ function parseKey(str, ptr, end = "=") {
8887
+ let dot = ptr - 1;
8888
+ let parsed = [];
8889
+ let endPtr = str.indexOf(end, ptr);
8890
+ if (endPtr < 0) {
8891
+ throw new TomlError("incomplete key-value: cannot find end of key", {
8892
+ toml: str,
8893
+ ptr
8894
+ });
8895
+ }
8896
+ do {
8897
+ let c = str[ptr = ++dot];
8898
+ if (c !== " " && c !== " ") {
8899
+ if (c === '"' || c === "'") {
8900
+ if (c === str[ptr + 1] && c === str[ptr + 2]) {
8901
+ throw new TomlError("multiline strings are not allowed in keys", {
8902
+ toml: str,
8903
+ ptr
8904
+ });
8905
+ }
8906
+ let eos = getStringEnd(str, ptr);
8907
+ if (eos < 0) {
8908
+ throw new TomlError("unfinished string encountered", {
8909
+ toml: str,
8910
+ ptr
8911
+ });
8912
+ }
8913
+ dot = str.indexOf(".", eos);
8914
+ let strEnd = str.slice(eos, dot < 0 || dot > endPtr ? endPtr : dot);
8915
+ let newLine = indexOfNewline(strEnd);
8916
+ if (newLine > -1) {
8917
+ throw new TomlError("newlines are not allowed in keys", {
8918
+ toml: str,
8919
+ ptr: ptr + dot + newLine
8920
+ });
8921
+ }
8922
+ if (strEnd.trimStart()) {
8923
+ throw new TomlError("found extra tokens after the string part", {
8924
+ toml: str,
8925
+ ptr: eos
8926
+ });
8927
+ }
8928
+ if (endPtr < eos) {
8929
+ endPtr = str.indexOf(end, eos);
8930
+ if (endPtr < 0) {
8931
+ throw new TomlError("incomplete key-value: cannot find end of key", {
8932
+ toml: str,
8933
+ ptr
8934
+ });
8935
+ }
8936
+ }
8937
+ parsed.push(parseString(str, ptr, eos));
8938
+ } else {
8939
+ dot = str.indexOf(".", ptr);
8940
+ let part = str.slice(ptr, dot < 0 || dot > endPtr ? endPtr : dot);
8941
+ if (!KEY_PART_RE.test(part)) {
8942
+ throw new TomlError("only letter, numbers, dashes and underscores are allowed in keys", {
8943
+ toml: str,
8944
+ ptr
8945
+ });
8946
+ }
8947
+ parsed.push(part.trimEnd());
8948
+ }
8949
+ }
8950
+ } while (dot + 1 && dot < endPtr);
8951
+ return [parsed, skipVoid(str, endPtr + 1, true, true)];
8952
+ }
8953
+ function parseInlineTable(str, ptr, depth, integersAsBigInt) {
8954
+ let res = {};
8955
+ let seen = /* @__PURE__ */ new Set();
8956
+ let c;
8957
+ ptr++;
8958
+ while ((c = str[ptr++]) !== "}" && c) {
8959
+ if (c === ",") {
8960
+ throw new TomlError("expected value, found comma", {
8961
+ toml: str,
8962
+ ptr: ptr - 1
8963
+ });
8964
+ } else if (c === "#")
8965
+ ptr = skipComment(str, ptr);
8966
+ else if (c !== " " && c !== " " && c !== "\n" && c !== "\r") {
8967
+ let k;
8968
+ let t = res;
8969
+ let hasOwn = false;
8970
+ let [key, keyEndPtr] = parseKey(str, ptr - 1);
8971
+ for (let i = 0; i < key.length; i++) {
8972
+ if (i)
8973
+ t = hasOwn ? t[k] : t[k] = {};
8974
+ k = key[i];
8975
+ if ((hasOwn = Object.hasOwn(t, k)) && (typeof t[k] !== "object" || seen.has(t[k]))) {
8976
+ throw new TomlError("trying to redefine an already defined value", {
8977
+ toml: str,
8978
+ ptr
8979
+ });
8980
+ }
8981
+ if (!hasOwn && k === "__proto__") {
8982
+ Object.defineProperty(t, k, { enumerable: true, configurable: true, writable: true });
8983
+ }
8984
+ }
8985
+ if (hasOwn) {
8986
+ throw new TomlError("trying to redefine an already defined value", {
8987
+ toml: str,
8988
+ ptr
8989
+ });
8990
+ }
8991
+ let [value, valueEndPtr] = extractValue(str, keyEndPtr, "}", depth - 1, integersAsBigInt);
8992
+ seen.add(value);
8993
+ t[k] = value;
8994
+ ptr = valueEndPtr;
8995
+ }
8996
+ }
8997
+ if (!c) {
8998
+ throw new TomlError("unfinished table encountered", {
8999
+ toml: str,
9000
+ ptr
9001
+ });
9002
+ }
9003
+ return [res, ptr];
8379
9004
  }
8380
- function containerOwnershipError(info, inventory) {
8381
- if (inventory.containerId && info.Id !== inventory.containerId) {
8382
- return { code: "CONTAINER_ID_MISMATCH", message: "container identity changed" };
9005
+ function parseArray(str, ptr, depth, integersAsBigInt) {
9006
+ let res = [];
9007
+ let c;
9008
+ ptr++;
9009
+ while ((c = str[ptr++]) !== "]" && c) {
9010
+ if (c === ",") {
9011
+ throw new TomlError("expected value, found comma", {
9012
+ toml: str,
9013
+ ptr: ptr - 1
9014
+ });
9015
+ } else if (c === "#")
9016
+ ptr = skipComment(str, ptr);
9017
+ else if (c !== " " && c !== " " && c !== "\n" && c !== "\r") {
9018
+ let e = extractValue(str, ptr - 1, "]", depth - 1, integersAsBigInt);
9019
+ res.push(e[0]);
9020
+ ptr = e[1];
9021
+ }
9022
+ }
9023
+ if (!c) {
9024
+ throw new TomlError("unfinished array encountered", {
9025
+ toml: str,
9026
+ ptr
9027
+ });
8383
9028
  }
8384
- if (info.Config?.Labels?.[INSTANCE_LABEL] !== "default") {
8385
- return { code: "CONTAINER_OWNERSHIP_MISMATCH", message: "container is not owned by this instance" };
9029
+ return [res, ptr];
9030
+ }
9031
+
9032
+ // ../../node_modules/.pnpm/smol-toml@1.6.0/node_modules/smol-toml/dist/parse.js
9033
+ function peekTable(key, table, meta, type) {
9034
+ let t = table;
9035
+ let m = meta;
9036
+ let k;
9037
+ let hasOwn = false;
9038
+ let state;
9039
+ for (let i = 0; i < key.length; i++) {
9040
+ if (i) {
9041
+ t = hasOwn ? t[k] : t[k] = {};
9042
+ m = (state = m[k]).c;
9043
+ if (type === 0 && (state.t === 1 || state.t === 2)) {
9044
+ return null;
9045
+ }
9046
+ if (state.t === 2) {
9047
+ let l = t.length - 1;
9048
+ t = t[l];
9049
+ m = m[l].c;
9050
+ }
9051
+ }
9052
+ k = key[i];
9053
+ if ((hasOwn = Object.hasOwn(t, k)) && m[k]?.t === 0 && m[k]?.d) {
9054
+ return null;
9055
+ }
9056
+ if (!hasOwn) {
9057
+ if (k === "__proto__") {
9058
+ Object.defineProperty(t, k, { enumerable: true, configurable: true, writable: true });
9059
+ Object.defineProperty(m, k, { enumerable: true, configurable: true, writable: true });
9060
+ }
9061
+ m[k] = {
9062
+ t: i < key.length - 1 && type === 2 ? 3 : type,
9063
+ d: false,
9064
+ i: 0,
9065
+ c: {}
9066
+ };
9067
+ }
8386
9068
  }
8387
- const expectedImage = inventory.imageDigest ?? inventory.imageRef;
8388
- if (info.Config?.Image !== expectedImage || info.Config?.Labels?.[IMAGE_LABEL] !== expectedImage) {
8389
- return { code: "CONTAINER_IMAGE_MISMATCH", message: "container image does not match inventory" };
9069
+ state = m[k];
9070
+ if (state.t !== type && !(type === 1 && state.t === 3)) {
9071
+ return null;
8390
9072
  }
8391
- const env = info.Config?.Env;
8392
- if (info.Config?.Hostname !== inventory.identity.hostname || exactEnv(env, "AGENT_WECHAT_MACHINE_ID") !== inventory.identity.machineId || exactEnv(env, "AGENT_WECHAT_HOSTNAME") !== inventory.identity.hostname || exactEnv(env, "AGENT_WECHAT_MAC") !== inventory.identity.mac) {
8393
- return { code: "CONTAINER_IDENTITY_MISMATCH", message: "container identity does not match inventory" };
9073
+ if (type === 2) {
9074
+ if (!state.d) {
9075
+ state.d = true;
9076
+ t[k] = [];
9077
+ }
9078
+ t[k].push(t = {});
9079
+ state.c[state.i++] = state = { t: 1, d: false, i: 0, c: {} };
8394
9080
  }
8395
- const mounts = new Map((info.Mounts ?? []).map((mount) => [mount.Destination, mount]));
8396
- const data = mounts.get("/data");
8397
- const home = mounts.get("/home/wechat");
8398
- const token = mounts.get("/data/auth-token");
8399
- if (data?.Type !== "volume" || data.Name !== inventory.volumes[0] || home?.Type !== "volume" || home.Name !== inventory.volumes[1] || token?.Type !== "bind" || token.Source !== inventory.tokenPath) {
8400
- return { code: "CONTAINER_RESOURCE_MISMATCH", message: "container mounts do not match inventory" };
9081
+ if (state.d) {
9082
+ return null;
8401
9083
  }
8402
- const bindings = info.HostConfig?.PortBindings?.[`${inventory.port}/tcp`];
8403
- if (!bindings || bindings.length !== 1 || bindings[0]?.HostPort !== String(inventory.port)) {
8404
- return { code: "CONTAINER_PORT_MISMATCH", message: "container port does not match inventory" };
9084
+ state.d = true;
9085
+ if (type === 1) {
9086
+ t = hasOwn ? t[k] : t[k] = {};
9087
+ } else if (type === 0 && hasOwn) {
9088
+ return null;
8405
9089
  }
8406
- return void 0;
9090
+ return [k, t, state.c];
8407
9091
  }
8408
- function volumeOwnershipError(info, expectedName, expectedRole) {
8409
- if (info.Name !== expectedName || info.Driver !== "local" || info.Labels?.[INSTANCE_LABEL] !== "default" || info.Labels?.[VOLUME_ROLE_LABEL] !== expectedRole) {
8410
- return { code: "VOLUME_OWNERSHIP_MISMATCH", message: `volume ${expectedName} is outside the trusted instance scope` };
9092
+ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
9093
+ let res = {};
9094
+ let meta = {};
9095
+ let tbl = res;
9096
+ let m = meta;
9097
+ for (let ptr = skipVoid(toml, 0); ptr < toml.length; ) {
9098
+ if (toml[ptr] === "[") {
9099
+ let isTableArray = toml[++ptr] === "[";
9100
+ let k = parseKey(toml, ptr += +isTableArray, "]");
9101
+ if (isTableArray) {
9102
+ if (toml[k[1] - 1] !== "]") {
9103
+ throw new TomlError("expected end of table declaration", {
9104
+ toml,
9105
+ ptr: k[1] - 1
9106
+ });
9107
+ }
9108
+ k[1]++;
9109
+ }
9110
+ let p = peekTable(
9111
+ k[0],
9112
+ res,
9113
+ meta,
9114
+ isTableArray ? 2 : 1
9115
+ /* Type.EXPLICIT */
9116
+ );
9117
+ if (!p) {
9118
+ throw new TomlError("trying to redefine an already defined table or value", {
9119
+ toml,
9120
+ ptr
9121
+ });
9122
+ }
9123
+ m = p[2];
9124
+ tbl = p[1];
9125
+ ptr = k[1];
9126
+ } else {
9127
+ let k = parseKey(toml, ptr);
9128
+ let p = peekTable(
9129
+ k[0],
9130
+ tbl,
9131
+ m,
9132
+ 0
9133
+ /* Type.DOTTED */
9134
+ );
9135
+ if (!p) {
9136
+ throw new TomlError("trying to redefine an already defined table or value", {
9137
+ toml,
9138
+ ptr
9139
+ });
9140
+ }
9141
+ let v = extractValue(toml, k[1], void 0, maxDepth, integersAsBigInt);
9142
+ p[1][p[0]] = v[0];
9143
+ ptr = v[1];
9144
+ }
9145
+ ptr = skipVoid(toml, ptr, true);
9146
+ if (toml[ptr] && toml[ptr] !== "\n" && toml[ptr] !== "\r") {
9147
+ throw new TomlError("each key-value declaration must be followed by an end-of-line", {
9148
+ toml,
9149
+ ptr
9150
+ });
9151
+ }
9152
+ ptr = skipVoid(toml, ptr);
8411
9153
  }
8412
- return void 0;
9154
+ return res;
9155
+ }
9156
+
9157
+ // ../../node_modules/.pnpm/smol-toml@1.6.0/node_modules/smol-toml/dist/stringify.js
9158
+ var BARE_KEY = /^[a-z0-9-_]+$/i;
9159
+ function extendedTypeOf(obj) {
9160
+ let type = typeof obj;
9161
+ if (type === "object") {
9162
+ if (Array.isArray(obj))
9163
+ return "array";
9164
+ if (obj instanceof Date)
9165
+ return "date";
9166
+ }
9167
+ return type;
9168
+ }
9169
+ function isArrayOfTables(obj) {
9170
+ for (let i = 0; i < obj.length; i++) {
9171
+ if (extendedTypeOf(obj[i]) !== "object")
9172
+ return false;
9173
+ }
9174
+ return obj.length != 0;
9175
+ }
9176
+ function formatString(s) {
9177
+ return JSON.stringify(s).replace(/\x7f/g, "\\u007f");
9178
+ }
9179
+ function stringifyValue(val, type, depth, numberAsFloat) {
9180
+ if (depth === 0) {
9181
+ throw new Error("Could not stringify the object: maximum object depth exceeded");
9182
+ }
9183
+ if (type === "number") {
9184
+ if (isNaN(val))
9185
+ return "nan";
9186
+ if (val === Infinity)
9187
+ return "inf";
9188
+ if (val === -Infinity)
9189
+ return "-inf";
9190
+ if (numberAsFloat && Number.isInteger(val))
9191
+ return val.toFixed(1);
9192
+ return val.toString();
9193
+ }
9194
+ if (type === "bigint" || type === "boolean") {
9195
+ return val.toString();
9196
+ }
9197
+ if (type === "string") {
9198
+ return formatString(val);
9199
+ }
9200
+ if (type === "date") {
9201
+ if (isNaN(val.getTime())) {
9202
+ throw new TypeError("cannot serialize invalid date");
9203
+ }
9204
+ return val.toISOString();
9205
+ }
9206
+ if (type === "object") {
9207
+ return stringifyInlineTable(val, depth, numberAsFloat);
9208
+ }
9209
+ if (type === "array") {
9210
+ return stringifyArray(val, depth, numberAsFloat);
9211
+ }
9212
+ }
9213
+ function stringifyInlineTable(obj, depth, numberAsFloat) {
9214
+ let keys = Object.keys(obj);
9215
+ if (keys.length === 0)
9216
+ return "{}";
9217
+ let res = "{ ";
9218
+ for (let i = 0; i < keys.length; i++) {
9219
+ let k = keys[i];
9220
+ if (i)
9221
+ res += ", ";
9222
+ res += BARE_KEY.test(k) ? k : formatString(k);
9223
+ res += " = ";
9224
+ res += stringifyValue(obj[k], extendedTypeOf(obj[k]), depth - 1, numberAsFloat);
9225
+ }
9226
+ return res + " }";
9227
+ }
9228
+ function stringifyArray(array, depth, numberAsFloat) {
9229
+ if (array.length === 0)
9230
+ return "[]";
9231
+ let res = "[ ";
9232
+ for (let i = 0; i < array.length; i++) {
9233
+ if (i)
9234
+ res += ", ";
9235
+ if (array[i] === null || array[i] === void 0) {
9236
+ throw new TypeError("arrays cannot contain null or undefined values");
9237
+ }
9238
+ res += stringifyValue(array[i], extendedTypeOf(array[i]), depth - 1, numberAsFloat);
9239
+ }
9240
+ return res + " ]";
9241
+ }
9242
+ function stringifyArrayTable(array, key, depth, numberAsFloat) {
9243
+ if (depth === 0) {
9244
+ throw new Error("Could not stringify the object: maximum object depth exceeded");
9245
+ }
9246
+ let res = "";
9247
+ for (let i = 0; i < array.length; i++) {
9248
+ res += `${res && "\n"}[[${key}]]
9249
+ `;
9250
+ res += stringifyTable(0, array[i], key, depth, numberAsFloat);
9251
+ }
9252
+ return res;
9253
+ }
9254
+ function stringifyTable(tableKey, obj, prefix, depth, numberAsFloat) {
9255
+ if (depth === 0) {
9256
+ throw new Error("Could not stringify the object: maximum object depth exceeded");
9257
+ }
9258
+ let preamble = "";
9259
+ let tables = "";
9260
+ let keys = Object.keys(obj);
9261
+ for (let i = 0; i < keys.length; i++) {
9262
+ let k = keys[i];
9263
+ if (obj[k] !== null && obj[k] !== void 0) {
9264
+ let type = extendedTypeOf(obj[k]);
9265
+ if (type === "symbol" || type === "function") {
9266
+ throw new TypeError(`cannot serialize values of type '${type}'`);
9267
+ }
9268
+ let key = BARE_KEY.test(k) ? k : formatString(k);
9269
+ if (type === "array" && isArrayOfTables(obj[k])) {
9270
+ tables += (tables && "\n") + stringifyArrayTable(obj[k], prefix ? `${prefix}.${key}` : key, depth - 1, numberAsFloat);
9271
+ } else if (type === "object") {
9272
+ let tblKey = prefix ? `${prefix}.${key}` : key;
9273
+ tables += (tables && "\n") + stringifyTable(tblKey, obj[k], tblKey, depth - 1, numberAsFloat);
9274
+ } else {
9275
+ preamble += key;
9276
+ preamble += " = ";
9277
+ preamble += stringifyValue(obj[k], type, depth, numberAsFloat);
9278
+ preamble += "\n";
9279
+ }
9280
+ }
9281
+ }
9282
+ if (tableKey && (preamble || !tables))
9283
+ preamble = preamble ? `[${tableKey}]
9284
+ ${preamble}` : `[${tableKey}]`;
9285
+ return preamble && tables ? `${preamble}
9286
+ ${tables}` : preamble || tables;
9287
+ }
9288
+ function stringify(obj, { maxDepth = 1e3, numbersAsFloat = false } = {}) {
9289
+ if (extendedTypeOf(obj) !== "object") {
9290
+ throw new TypeError("stringify can only be called with an object");
9291
+ }
9292
+ let str = stringifyTable(0, obj, "", maxDepth, numbersAsFloat);
9293
+ if (str[str.length - 1] !== "\n")
9294
+ return str + "\n";
9295
+ return str;
9296
+ }
9297
+
9298
+ // ../../node_modules/.pnpm/smol-toml@1.6.0/node_modules/smol-toml/dist/index.js
9299
+ var dist_default = { parse, stringify, TomlDate, TomlError };
9300
+
9301
+ // src/outbound-config.ts
9302
+ var OUTBOUND_ENV_KEYS = [
9303
+ "AGENT_WECHAT_OUTBOUND_QUEUE_CAPACITY",
9304
+ "AGENT_WECHAT_OUTBOUND_MIN_SPACING_MS",
9305
+ "AGENT_WECHAT_OUTBOUND_JITTER_MS",
9306
+ "AGENT_WECHAT_CHAT_COOLDOWN_MS",
9307
+ "AGENT_WECHAT_HOURLY_BUDGET",
9308
+ "AGENT_WECHAT_DAILY_BUDGET",
9309
+ "AGENT_WECHAT_QUIET_START_MIN",
9310
+ "AGENT_WECHAT_QUIET_END_MIN",
9311
+ "AGENT_WECHAT_OUTBOUND_LONG_TAIL_JITTER_MS",
9312
+ "AGENT_WECHAT_OUTBOUND_LONG_TAIL_CHANCE_PERCENT",
9313
+ "AGENT_WECHAT_OUTBOUND_TASK_TTL_MS",
9314
+ "AGENT_WECHAT_OUTBOUND_IDEMPOTENCY_TTL_MS",
9315
+ "AGENT_WECHAT_OUTBOUND_IDEMPOTENCY_MAX_ROWS",
9316
+ "AGENT_WECHAT_OUTBOUND_DISABLED"
9317
+ ];
9318
+ var aliases = {
9319
+ queueCapacity: "AGENT_WECHAT_OUTBOUND_QUEUE_CAPACITY",
9320
+ minSpacingMs: "AGENT_WECHAT_OUTBOUND_MIN_SPACING_MS",
9321
+ jitterMs: "AGENT_WECHAT_OUTBOUND_JITTER_MS",
9322
+ chatCooldownMs: "AGENT_WECHAT_CHAT_COOLDOWN_MS",
9323
+ hourlyBudget: "AGENT_WECHAT_HOURLY_BUDGET",
9324
+ dailyBudget: "AGENT_WECHAT_DAILY_BUDGET",
9325
+ quietStartMin: "AGENT_WECHAT_QUIET_START_MIN",
9326
+ quietEndMin: "AGENT_WECHAT_QUIET_END_MIN",
9327
+ longTailJitterMs: "AGENT_WECHAT_OUTBOUND_LONG_TAIL_JITTER_MS",
9328
+ longTailChancePercent: "AGENT_WECHAT_OUTBOUND_LONG_TAIL_CHANCE_PERCENT",
9329
+ taskTtlMs: "AGENT_WECHAT_OUTBOUND_TASK_TTL_MS",
9330
+ idempotencyTtlMs: "AGENT_WECHAT_OUTBOUND_IDEMPOTENCY_TTL_MS",
9331
+ idempotencyMaxRows: "AGENT_WECHAT_OUTBOUND_IDEMPOTENCY_MAX_ROWS",
9332
+ disabled: "AGENT_WECHAT_OUTBOUND_DISABLED"
9333
+ };
9334
+ function readOutboundConfig(file) {
9335
+ if (!file) return {};
9336
+ const parsed = dist_default.parse(fs3.readFileSync(path3.resolve(file), "utf8"));
9337
+ const result = {};
9338
+ for (const [key, env] of Object.entries(aliases)) if (parsed[key] !== void 0) result[env] = String(parsed[key]);
9339
+ for (const key of OUTBOUND_ENV_KEYS) if (parsed[key] !== void 0) result[key] = String(parsed[key]);
9340
+ return result;
9341
+ }
9342
+ function outboundFromEnvEntries(entries) {
9343
+ const result = {};
9344
+ const allowed = new Set(OUTBOUND_ENV_KEYS);
9345
+ for (const entry of entries) {
9346
+ const index = entry.indexOf("=");
9347
+ if (index > 0 && allowed.has(entry.slice(0, index))) result[entry.slice(0, index)] = entry.slice(index + 1);
9348
+ }
9349
+ return result;
9350
+ }
9351
+ function resolveOutboundConfig(file, cli = {}) {
9352
+ const config = file ? readOutboundConfig(file) : {};
9353
+ const env = {};
9354
+ for (const key of OUTBOUND_ENV_KEYS) if (process.env[key] !== void 0) env[key] = process.env[key];
9355
+ return { ...config, ...env, ...cli };
9356
+ }
9357
+
9358
+ // src/lifecycle-policy.ts
9359
+ var INSTANCE_LABEL = "dev.visionclaw.agent-wechat.instance";
9360
+ var IMAGE_LABEL = "dev.visionclaw.agent-wechat.image";
9361
+ var VOLUME_ROLE_LABEL = "dev.visionclaw.agent-wechat.volume-role";
9362
+ function hasOwnedContainer(info) {
9363
+ return info.Config?.Labels?.[INSTANCE_LABEL] === "default";
9364
+ }
9365
+ function isReconcileableContainer(info, inventory) {
9366
+ if (hasOwnedContainer(info)) return true;
9367
+ const id = inventory?.containerId;
9368
+ return Boolean(id && (info.Id === id || info.Id.startsWith(id) || id.startsWith(info.Id)));
9369
+ }
9370
+ function hasOwnedVolume(existing, name, role) {
9371
+ return existing.Name === name && existing.Driver === "local" && existing.Labels?.[INSTANCE_LABEL] === "default" && existing.Labels?.[VOLUME_ROLE_LABEL] === role;
8413
9372
  }
8414
9373
 
8415
9374
  // src/lifecycle.ts
@@ -8490,8 +9449,12 @@ function ensureOwnedVolumes(inventory) {
8490
9449
  docker(["volume", "create", "--driver", "local", "--label", `${INSTANCE_LABEL}=default`, "--label", `${VOLUME_ROLE_LABEL}=${roles[index]}`, name]);
8491
9450
  continue;
8492
9451
  }
8493
- const violation = volumeOwnershipError(existing, name, roles[index]);
8494
- if (violation) throw new CliError(violation.code, violation.message, EXIT.ENVIRONMENT);
9452
+ assertVolumeOwnership(existing, name, roles[index]);
9453
+ }
9454
+ }
9455
+ function assertVolumeOwnership(existing, name, role) {
9456
+ if (!hasOwnedVolume(existing, name, role)) {
9457
+ throw new CliError("VOLUME_OWNERSHIP_MISMATCH", `refusing to use volume ${name} without trusted ownership`, EXIT.ENVIRONMENT);
8495
9458
  }
8496
9459
  }
8497
9460
  function assertOwnedVolumes(inventory) {
@@ -8499,10 +9462,12 @@ function assertOwnedVolumes(inventory) {
8499
9462
  for (const [index, name] of inventory.volumes.entries()) {
8500
9463
  const existing = inspectVolume(name);
8501
9464
  if (!existing) continue;
8502
- const violation = volumeOwnershipError(existing, name, roles[index]);
8503
- if (violation) throw new CliError(violation.code, violation.message, EXIT.CLEANUP);
9465
+ assertVolumeOwnership(existing, name, roles[index]);
8504
9466
  }
8505
9467
  }
9468
+ function assertOwnedContainer(info, inventory) {
9469
+ if (!isReconcileableContainer(info, inventory)) throw new CliError("CONTAINER_OWNERSHIP_MISMATCH", "refusing to operate on an unowned container with the fixed name", EXIT.ENVIRONMENT);
9470
+ }
8506
9471
  function clearContainerIdentity(inventory) {
8507
9472
  const info = inspectContainer();
8508
9473
  if (!info) throw new CliError("INSTANCE_NOT_RUNNING", "trusted container is required to clear container identity", EXIT.CLEANUP);
@@ -8519,8 +9484,7 @@ function removeOwnedVolume(inventory, index, role) {
8519
9484
  const name = inventory.volumes[index];
8520
9485
  const existing = inspectVolume(name);
8521
9486
  if (!existing) return false;
8522
- const violation = volumeOwnershipError(existing, name, role);
8523
- if (violation) throw new CliError(violation.code, violation.message, EXIT.CLEANUP);
9487
+ assertVolumeOwnership(existing, name, role);
8524
9488
  docker(["volume", "rm", name]);
8525
9489
  return true;
8526
9490
  }
@@ -8542,10 +9506,6 @@ function resolveImage(options) {
8542
9506
  if (published && !digest) throw new CliError("IMAGE_DIGEST_UNRESOLVED", "published image did not resolve to a fork repo digest", EXIT.ENVIRONMENT);
8543
9507
  return { runReference: digest || requested, requestedReference: requested, digest };
8544
9508
  }
8545
- function assertOwnedContainer(info, inventory) {
8546
- const violation = containerOwnershipError(info, inventory);
8547
- if (violation) throw new CliError(violation.code, violation.message, EXIT.ENVIRONMENT);
8548
- }
8549
9509
  async function waitCompatible(token, timeoutMs = 3e4) {
8550
9510
  await waitHealthy(timeoutMs);
8551
9511
  try {
@@ -8572,21 +9532,19 @@ async function waitHealthy(timeoutMs = 3e4) {
8572
9532
  }
8573
9533
  async function startInstance(options) {
8574
9534
  if (!dockerAvailable()) throw new CliError("DOCKER_UNAVAILABLE", "Docker daemon is unavailable", EXIT.ENVIRONMENT);
8575
- const selected = resolveImage({ explicit: options.image, pull: options.pull, noPull: options.noPull, localDefault: options.localDefault });
8576
9535
  const existing = inspectContainer();
8577
9536
  const current = loadInventory();
8578
9537
  if (existing) {
8579
- if (!current) throw new CliError("INSTANCE_INVENTORY_MISSING", "existing container has no trusted inventory", EXIT.ENVIRONMENT);
8580
9538
  assertOwnedContainer(existing, current);
8581
- const expected = selected.digest || selected.runReference;
8582
- const recorded = existing.Config?.Labels?.["dev.visionclaw.agent-wechat.image"];
8583
- if (existing.Config?.Image !== expected || recorded !== expected) {
8584
- throw new CliError("IMAGE_COMPATIBILITY_MISMATCH", "existing container image does not match requested compatible image", EXIT.ENVIRONMENT);
9539
+ if (options.outbound && Object.keys(options.outbound).length > 0) {
9540
+ throw new CliError("OUTBOUND_RESTART_REQUIRED", "outbound policy changed; run wx restart to recreate the container", EXIT.ARGUMENT);
8585
9541
  }
8586
- if (!existing.State?.Running) docker(["start", existing.Id], { inherit: true });
9542
+ if (!current) throw new CliError("INSTANCE_INVENTORY_MISSING", "existing container has no trusted inventory", EXIT.ENVIRONMENT);
9543
+ if (!existing.State?.Running) docker(["start", CONTAINER_NAME], { inherit: true });
8587
9544
  await waitCompatible(options.token);
8588
9545
  return current;
8589
9546
  }
9547
+ const selected = resolveImage({ explicit: options.image, pull: options.pull, noPull: options.noPull, localDefault: options.localDefault });
8590
9548
  const provisional = createInventory(selected.requestedReference, options.identity, selected.digest);
8591
9549
  saveInventory(provisional);
8592
9550
  ensureOwnedVolumes(provisional);
@@ -8595,7 +9553,8 @@ async function startInstance(options) {
8595
9553
  containerName: CONTAINER_NAME,
8596
9554
  tokenPath: TOKEN_PATH,
8597
9555
  port: DEFAULT_PORT,
8598
- proxy: options.proxy
9556
+ proxy: options.proxy,
9557
+ outbound: options.outbound
8599
9558
  });
8600
9559
  args.splice(args.length - 1, 0, "--label", `${INSTANCE_LABEL}=default`, "--label", `${IMAGE_LABEL}=${selected.digest || selected.runReference}`);
8601
9560
  docker(args, { inherit: true });
@@ -8612,13 +9571,28 @@ async function startInstance(options) {
8612
9571
  }
8613
9572
  return inventory;
8614
9573
  }
9574
+ function outboundFromContainer(info) {
9575
+ return outboundFromEnvEntries(info.Config?.Env ?? []);
9576
+ }
9577
+ function stopContainer(idOrName) {
9578
+ docker(["stop", idOrName], { inherit: true });
9579
+ }
9580
+ function renameContainer(id, name) {
9581
+ docker(["rename", id, name], { inherit: true });
9582
+ }
9583
+ function removeContainer(id) {
9584
+ docker(["rm", "-f", id], { inherit: true });
9585
+ }
9586
+ function startContainer(idOrName) {
9587
+ docker(["start", idOrName], { inherit: true });
9588
+ }
8615
9589
  function stopInstance() {
8616
9590
  if (!dockerAvailable()) throw new CliError("DOCKER_UNAVAILABLE", "Docker daemon is unavailable", EXIT.ENVIRONMENT);
8617
9591
  const info = inspectContainer();
8618
9592
  if (!info) return { stopped: false };
8619
9593
  const inventory = loadInventory();
8620
- if (!inventory) throw new CliError("INSTANCE_INVENTORY_MISSING", "refusing to remove an unowned container", EXIT.ENVIRONMENT);
8621
9594
  assertOwnedContainer(info, inventory);
9595
+ if (!inventory) throw new CliError("INSTANCE_INVENTORY_MISSING", "refusing to remove an unowned container", EXIT.ENVIRONMENT);
8622
9596
  docker(["rm", "-f", info.Id], { inherit: true });
8623
9597
  return { stopped: true };
8624
9598
  }
@@ -8637,18 +9611,18 @@ function purgeInstance() {
8637
9611
  remaining.push(`volume:${volume}`);
8638
9612
  }
8639
9613
  }
8640
- for (const file of [TOKEN_PATH, path3.join(CONFIG_DIR, "device-identity.env"), path3.join(CONFIG_DIR, "device-identity.json")]) {
9614
+ for (const file of [TOKEN_PATH, path4.join(CONFIG_DIR, "device-identity.env"), path4.join(CONFIG_DIR, "device-identity.json")]) {
8641
9615
  try {
8642
9616
  assertSafePurgePath(file, CONFIG_DIR);
8643
9617
  } catch {
8644
- remaining.push(path3.basename(file));
9618
+ remaining.push(path4.basename(file));
8645
9619
  continue;
8646
9620
  }
8647
9621
  try {
8648
- fs3.unlinkSync(file);
8649
- removed.push(path3.basename(file));
9622
+ fs4.unlinkSync(file);
9623
+ removed.push(path4.basename(file));
8650
9624
  } catch (error) {
8651
- if (error.code !== "ENOENT") remaining.push(path3.basename(file));
9625
+ if (error.code !== "ENOENT") remaining.push(path4.basename(file));
8652
9626
  }
8653
9627
  }
8654
9628
  if (remaining.length === 0) removeInventory();
@@ -8660,8 +9634,9 @@ async function replaceImage(options) {
8660
9634
  if (!previous) throw new CliError("INSTANCE_INVENTORY_MISSING", "start the instance before image upgrade", EXIT.ENVIRONMENT);
8661
9635
  const info = inspectContainer();
8662
9636
  if (info) assertOwnedContainer(info, previous);
9637
+ const reconciled = previous;
8663
9638
  const selected = resolveImage({ explicit: options.image, pull: true, localDefault: previous.imageRef });
8664
- if (selected.digest === previous.imageDigest) return previous;
9639
+ if (selected.digest === reconciled.imageDigest) return reconciled;
8665
9640
  if (info) {
8666
9641
  if (info.State?.Running) docker(["stop", info.Id], { inherit: true });
8667
9642
  docker(["rename", info.Id, `${CONTAINER_NAME}-rollback`]);
@@ -8675,7 +9650,7 @@ async function replaceImage(options) {
8675
9650
  const failed = inspectContainer();
8676
9651
  if (failed) docker(["rm", "-f", failed.Id]);
8677
9652
  if (info) docker(["rename", info.Id, CONTAINER_NAME]);
8678
- saveInventory(previous);
9653
+ saveInventory(reconciled);
8679
9654
  if (info?.State?.Running) docker(["start", CONTAINER_NAME]);
8680
9655
  } catch {
8681
9656
  throw new CliError("ROLLBACK_FAILED", "image upgrade failed and rollback was incomplete", EXIT.ROLLBACK);
@@ -8761,12 +9736,12 @@ function checkCliUpgrade(currentVersion, readLatest = () => execFileSync2("npm",
8761
9736
  }
8762
9737
 
8763
9738
  // src/cli.ts
8764
- var VERSION = false ? "0.0.0-test" : "0.13.3";
9739
+ var VERSION = false ? "0.0.0-test" : "0.14.0";
8765
9740
  var MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
8766
9741
  function readToken() {
8767
9742
  try {
8768
9743
  secureRegularFile(TOKEN_PATH);
8769
- const value = fs4.readFileSync(TOKEN_PATH, "utf8").trim();
9744
+ const value = fs5.readFileSync(TOKEN_PATH, "utf8").trim();
8770
9745
  if (!/^[a-f0-9]{64}$/.test(value)) throw new Error("INVALID_AUTH_TOKEN");
8771
9746
  return value;
8772
9747
  } catch (error) {
@@ -8778,9 +9753,9 @@ function readToken() {
8778
9753
  function ensureToken() {
8779
9754
  const current = readToken();
8780
9755
  if (current) return current;
8781
- fs4.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
9756
+ fs5.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
8782
9757
  const token = randomBytes2(32).toString("hex");
8783
- fs4.writeFileSync(TOKEN_PATH, `${token}
9758
+ fs5.writeFileSync(TOKEN_PATH, `${token}
8784
9759
  `, { mode: 384, flag: "wx" });
8785
9760
  return token;
8786
9761
  }
@@ -8880,13 +9855,13 @@ async function login(timeoutMs, newAccount) {
8880
9855
  function regularUpload(target) {
8881
9856
  let stat;
8882
9857
  try {
8883
- stat = fs4.lstatSync(target);
9858
+ stat = fs5.lstatSync(target);
8884
9859
  } catch {
8885
9860
  throw new CliError("FILE_NOT_FOUND", `file not found: ${target}`, EXIT.ARGUMENT);
8886
9861
  }
8887
9862
  if (!stat.isFile() || stat.isSymbolicLink()) throw new CliError("INVALID_UPLOAD_FILE", "upload must be a regular non-symlink file", EXIT.ARGUMENT);
8888
9863
  if (stat.size > MAX_UPLOAD_BYTES) throw new CliError("UPLOAD_TOO_LARGE", `upload exceeds ${MAX_UPLOAD_BYTES} bytes`, EXIT.ARGUMENT);
8889
- return fs4.readFileSync(target);
9864
+ return fs5.readFileSync(target);
8890
9865
  }
8891
9866
  function renderMessages(page) {
8892
9867
  for (const item of [...page.items].reverse()) console.log(`${item.localId} ${item.timestamp} ${item.senderName || item.sender || ""} ${item.content}`);
@@ -8903,15 +9878,15 @@ async function resetAuth(yes) {
8903
9878
  try {
8904
9879
  removeOwnedVolume(inventory, 1, "wechat-home");
8905
9880
  } catch (error) {
8906
- if (error instanceof CliError && error.code === "VOLUME_OWNERSHIP_MISMATCH") throw error;
9881
+ if (error instanceof CliError && error.code === "VOLUME_DRIVER_UNSUPPORTED") throw error;
8907
9882
  throw new CliError("AUTH_RESET_CLEANUP_INCOMPLETE", `remaining resource: volume:${inventory.volumes[1]}`, EXIT.CLEANUP);
8908
9883
  }
8909
9884
  const remaining = [];
8910
9885
  for (const file of ["device-identity.env", "device-identity.json"]) {
8911
- const target = path4.join(CONFIG_DIR, file);
9886
+ const target = path5.join(CONFIG_DIR, file);
8912
9887
  try {
8913
9888
  assertSafePurgePath(target, CONFIG_DIR);
8914
- fs4.unlinkSync(target);
9889
+ fs5.unlinkSync(target);
8915
9890
  } catch (error) {
8916
9891
  if (error.code !== "ENOENT") remaining.push(file);
8917
9892
  }
@@ -8926,7 +9901,6 @@ function programStatus() {
8926
9901
  const container = inspectContainer();
8927
9902
  if (container) {
8928
9903
  if (!inventory) throw new CliError("INSTANCE_INVENTORY_MISSING", "existing container has no trusted inventory", EXIT.ENVIRONMENT);
8929
- assertOwnedContainer(container, inventory);
8930
9904
  }
8931
9905
  return {
8932
9906
  cliVersion: VERSION,
@@ -8959,9 +9933,9 @@ program2.name("wx").description("Single-instance agent-wechat CLI").version(VERS
8959
9933
  program2.showSuggestionAfterError();
8960
9934
  program2.configureOutput({ outputError: (text) => process.stderr.write(text) });
8961
9935
  program2.exitOverride();
8962
- program2.command("start").description("Start the compatible single-instance container").option("--image <reference>", `exact ${GHCR_IMAGE} semver tag or digest`).option("--pull", "pull and resolve the selected image").option("--offline", "offline mode; never pull").option("--proxy <url>", "transparent proxy URL").action((options) => action(async () => {
9936
+ program2.command("start").description("Start the compatible single-instance container").option("--image <reference>", `exact ${GHCR_IMAGE} semver/commit tag or digest`).option("--pull", "pull and resolve the selected image").option("--offline", "offline mode; never pull").option("--proxy <url>", "transparent proxy URL").option("--outbound-config <file>", "TOML outbound policy file (never logged)").option("--outbound-queue-capacity <count>", "outbound queue capacity").option("--outbound-disabled", "disable outbound sending").option("--outbound-quiet-start <minutes>", "quiet-hours start minute").option("--outbound-quiet-end <minutes>", "quiet-hours end minute").option("--outbound-hourly-budget <count>", "hourly outbound budget").option("--outbound-daily-budget <count>", "daily outbound budget").option("--outbound-chat-cooldown-ms <ms>", "per-chat cooldown").option("--outbound-min-spacing-ms <ms>", "minimum spacing").option("--outbound-jitter-ms <ms>", "spacing jitter").option("--outbound-long-tail-jitter-ms <ms>", "long-tail jitter").option("--outbound-long-tail-chance-percent <percent>", "long-tail chance").option("--outbound-task-ttl-ms <ms>", "task TTL").option("--outbound-idempotency-ttl-ms <ms>", "idempotency TTL").option("--outbound-idempotency-max-rows <rows>", "idempotency max rows").action((options) => action(async () => {
8963
9937
  if (options.pull && options.offline) throw new CliError("ARGUMENT_CONFLICT", "--pull and --offline are mutually exclusive", EXIT.ARGUMENT);
8964
- const inventory = await startInstance({ identity: ensureDeviceIdentity(CONFIG_DIR), token: ensureToken(), localDefault: localBuildImage(), noPull: options.offline === true, ...options });
9938
+ const inventory = await startInstance({ identity: ensureDeviceIdentity(CONFIG_DIR), token: ensureToken(), localDefault: localBuildImage(), noPull: options.offline === true, outbound: resolveOutboundConfig(options.outboundConfig, Object.fromEntries([["AGENT_WECHAT_OUTBOUND_QUEUE_CAPACITY", options.outboundQueueCapacity], ["AGENT_WECHAT_OUTBOUND_DISABLED", options.outboundDisabled ? "true" : void 0], ["AGENT_WECHAT_QUIET_START_MIN", options.outboundQuietStart], ["AGENT_WECHAT_QUIET_END_MIN", options.outboundQuietEnd], ["AGENT_WECHAT_HOURLY_BUDGET", options.outboundHourlyBudget], ["AGENT_WECHAT_DAILY_BUDGET", options.outboundDailyBudget], ["AGENT_WECHAT_CHAT_COOLDOWN_MS", options.outboundChatCooldownMs], ["AGENT_WECHAT_OUTBOUND_MIN_SPACING_MS", options.outboundMinSpacingMs], ["AGENT_WECHAT_OUTBOUND_JITTER_MS", options.outboundJitterMs], ["AGENT_WECHAT_OUTBOUND_LONG_TAIL_JITTER_MS", options.outboundLongTailJitterMs], ["AGENT_WECHAT_OUTBOUND_LONG_TAIL_CHANCE_PERCENT", options.outboundLongTailChancePercent], ["AGENT_WECHAT_OUTBOUND_TASK_TTL_MS", options.outboundTaskTtlMs], ["AGENT_WECHAT_OUTBOUND_IDEMPOTENCY_TTL_MS", options.outboundIdempotencyTtlMs], ["AGENT_WECHAT_OUTBOUND_IDEMPOTENCY_MAX_ROWS", options.outboundIdempotencyMaxRows]].filter(([, v]) => v !== void 0))), ...options });
8965
9939
  output({ container: inventory.containerName, imageDigest: inventory.imageDigest, port: inventory.port }, () => console.log(`Started ${inventory.containerName} on port ${inventory.port}`));
8966
9940
  }));
8967
9941
  program2.command("stop").description("Stop and remove the container; preserve data unless --purge").option("--purge", "delete every resource in the trusted default-instance inventory").option("--yes", "skip only the interactive confirmation").action((options) => action(async () => {
@@ -8976,12 +9950,59 @@ program2.command("stop").description("Stop and remove the container; preserve da
8976
9950
  output(result, () => console.log(result.stopped ? "Container stopped and removed" : "Container is absent"));
8977
9951
  }
8978
9952
  }));
8979
- program2.command("restart").description("Restart while preserving data").action(() => action(async () => {
9953
+ program2.command("restart").description("Restart while preserving data").option("--outbound-config <file>", "TOML outbound policy file (never logged)").option("--outbound-queue-capacity <count>", "outbound queue capacity").option("--outbound-disabled", "disable outbound sending").option("--outbound-quiet-start <minutes>").option("--outbound-quiet-end <minutes>").option("--outbound-hourly-budget <count>").option("--outbound-daily-budget <count>").option("--outbound-chat-cooldown-ms <ms>").option("--outbound-min-spacing-ms <ms>").option("--outbound-jitter-ms <ms>").option("--outbound-long-tail-jitter-ms <ms>").option("--outbound-long-tail-chance-percent <percent>").option("--outbound-task-ttl-ms <ms>").option("--outbound-idempotency-ttl-ms <ms>").option("--outbound-idempotency-max-rows <rows>").action((options) => action(async () => {
8980
9954
  const inventory = loadInventory();
8981
9955
  if (!inventory) throw new CliError("INSTANCE_INVENTORY_MISSING", "start the instance first", EXIT.SERVICE);
8982
- stopInstance();
8983
- const result = await startInstance({ identity: ensureDeviceIdentity(CONFIG_DIR), token: ensureToken(), image: inventory.imageDigest || inventory.imageRef, noPull: true, localDefault: localBuildImage() });
8984
- output({ restarted: true, imageDigest: result.imageDigest }, () => console.log("Instance restarted"));
9956
+ const oldContainer = inspectContainer();
9957
+ const cliOverrides = Object.fromEntries([
9958
+ ["AGENT_WECHAT_OUTBOUND_QUEUE_CAPACITY", options.outboundQueueCapacity],
9959
+ ["AGENT_WECHAT_OUTBOUND_DISABLED", options.outboundDisabled ? "true" : void 0],
9960
+ ["AGENT_WECHAT_QUIET_START_MIN", options.outboundQuietStart],
9961
+ ["AGENT_WECHAT_QUIET_END_MIN", options.outboundQuietEnd],
9962
+ ["AGENT_WECHAT_HOURLY_BUDGET", options.outboundHourlyBudget],
9963
+ ["AGENT_WECHAT_DAILY_BUDGET", options.outboundDailyBudget],
9964
+ ["AGENT_WECHAT_CHAT_COOLDOWN_MS", options.outboundChatCooldownMs],
9965
+ ["AGENT_WECHAT_OUTBOUND_MIN_SPACING_MS", options.outboundMinSpacingMs],
9966
+ ["AGENT_WECHAT_OUTBOUND_JITTER_MS", options.outboundJitterMs],
9967
+ ["AGENT_WECHAT_OUTBOUND_LONG_TAIL_JITTER_MS", options.outboundLongTailJitterMs],
9968
+ ["AGENT_WECHAT_OUTBOUND_LONG_TAIL_CHANCE_PERCENT", options.outboundLongTailChancePercent],
9969
+ ["AGENT_WECHAT_OUTBOUND_TASK_TTL_MS", options.outboundTaskTtlMs],
9970
+ ["AGENT_WECHAT_OUTBOUND_IDEMPOTENCY_TTL_MS", options.outboundIdempotencyTtlMs],
9971
+ ["AGENT_WECHAT_OUTBOUND_IDEMPOTENCY_MAX_ROWS", options.outboundIdempotencyMaxRows]
9972
+ ].filter(([, v]) => v !== void 0));
9973
+ const outbound = resolveOutboundConfig(options.outboundConfig, cliOverrides);
9974
+ const oldOutbound = oldContainer ? outboundFromContainer(oldContainer) : {};
9975
+ const effectiveOutbound = { ...oldOutbound, ...outbound };
9976
+ const previousInventory = inventory;
9977
+ const rollbackName = `${CONTAINER_NAME}-rollback`;
9978
+ let oldStopped = false;
9979
+ let oldRenamed = false;
9980
+ try {
9981
+ if (oldContainer) {
9982
+ if (oldContainer.State?.Running) {
9983
+ stopContainer(oldContainer.Id);
9984
+ oldStopped = true;
9985
+ }
9986
+ renameContainer(oldContainer.Id, rollbackName);
9987
+ oldRenamed = true;
9988
+ }
9989
+ const result = await startInstance({ identity: ensureDeviceIdentity(CONFIG_DIR), token: ensureToken(), image: inventory.imageDigest || inventory.imageRef, noPull: true, localDefault: localBuildImage(), outbound: effectiveOutbound });
9990
+ if (oldRenamed) removeContainer(rollbackName);
9991
+ output({ restarted: true, imageDigest: result.imageDigest }, () => console.log("Instance restarted"));
9992
+ } catch (error) {
9993
+ try {
9994
+ const failed = inspectContainer();
9995
+ if (failed && failed.Id !== oldContainer?.Id) removeContainer(failed.Id);
9996
+ if (oldContainer) {
9997
+ if (oldRenamed) renameContainer(rollbackName, CONTAINER_NAME);
9998
+ if (oldStopped && oldContainer.State?.Running) startContainer(CONTAINER_NAME);
9999
+ }
10000
+ saveInventory(previousInventory);
10001
+ } catch {
10002
+ throw new CliError("ROLLBACK_FAILED", "restart failed and rollback was incomplete", EXIT.ROLLBACK);
10003
+ }
10004
+ throw error;
10005
+ }
8985
10006
  }));
8986
10007
  program2.command("status").description("Show read-only lifecycle and authentication status").action(() => action(async () => {
8987
10008
  const status = programStatus();
@@ -9008,7 +10029,7 @@ program2.command("doctor").description("Run read-only environment and compatibil
9008
10029
  const checks = programStatus();
9009
10030
  checks.architecture = os2.arch();
9010
10031
  checks.token = readToken() ? "present" : "absent";
9011
- checks.configPermissions = fs4.existsSync(CONFIG_DIR) ? (fs4.statSync(CONFIG_DIR).mode & 511).toString(8) : "absent";
10032
+ checks.configPermissions = fs5.existsSync(CONFIG_DIR) ? (fs5.statSync(CONFIG_DIR).mode & 511).toString(8) : "absent";
9012
10033
  if (checks.container === "running") {
9013
10034
  try {
9014
10035
  checks.health = (await fetch(`http://localhost:${DEFAULT_PORT}/health`)).ok ? "ok" : "failed";
@@ -9067,6 +10088,13 @@ chats.command("mark-read <chat-id>").action((chatId) => action(async () => {
9067
10088
  if (!result.ok) throw new CliError(result.errorCode || "MARK_READ_FAILED", result.error || "mark-read was not verified", EXIT.SERVICE);
9068
10089
  output(result, () => console.log(`${chatId}: ${result.beforeUnread} -> ${result.afterUnread}`));
9069
10090
  }));
10091
+ chats.command("members <group-id>").description("List a bounded page of group members read-only").option("--limit <number>", "page size", "50").option("--cursor <cursor>", "opaque next-page cursor").action((groupId, options) => action(async () => {
10092
+ const page = await client().listGroupMembersPage(groupId, integer(options.limit, "limit", 1, 100), options.cursor);
10093
+ output(page, () => {
10094
+ for (const member of page.items) console.log(`${member.memberId} ${member.displayName} ${member.groupAlias ?? ""}`);
10095
+ if (page.nextCursor) console.error(`next cursor: ${page.nextCursor}`);
10096
+ });
10097
+ }));
9070
10098
  var contacts = program2.command("contacts").description("List contacts read-only").option("--limit <number>", "page size", "200").option("--cursor <cursor>", "opaque next-page cursor").action((options) => action(async () => {
9071
10099
  const page = await client().listContactsPage(integer(options.limit, "limit", 1, 200), options.cursor);
9072
10100
  output(page, () => {
@@ -9083,8 +10111,8 @@ program2.command("messages <chat-id>").description("Read messages without openin
9083
10111
  output(page, () => renderMessages(page));
9084
10112
  }));
9085
10113
  program2.command("send <chat-id>").description("Send exactly one payload to a stable chat ID").option("--text <message>").option("--image <path>").option("--file <path>").option("--idempotency-key <key>", "stable key for retry/reconciliation").option("--confirm-similar", "confirm operator-reviewed similar text").action((chatId, options) => action(async () => {
9086
- const image = options.image ? { data: regularUpload(options.image).toString("base64"), mimeType: path4.extname(options.image).toLowerCase() === ".jpg" ? "image/jpeg" : "image/png" } : void 0;
9087
- const file = options.file ? { data: regularUpload(options.file).toString("base64"), filename: path4.basename(options.file) } : void 0;
10114
+ const image = options.image ? { data: regularUpload(options.image).toString("base64"), mimeType: path5.extname(options.image).toLowerCase() === ".jpg" ? "image/jpeg" : "image/png" } : void 0;
10115
+ const file = options.file ? { data: regularUpload(options.file).toString("base64"), filename: path5.basename(options.file) } : void 0;
9088
10116
  let params;
9089
10117
  try {
9090
10118
  params = buildCliSendParams({ chatId, text: options.text, image, file, confirmSimilar: options.confirmSimilar, idempotencyKey: options.idempotencyKey });
@@ -9098,7 +10126,7 @@ program2.command("send <chat-id>").description("Send exactly one payload to a st
9098
10126
  }
9099
10127
  output(result, () => console.log("Message sent"));
9100
10128
  }));
9101
- program2.command("upgrade").description("Check CLI/image upgrades without claiming atomic self-upgrade").addOption(new Option("--check", "read-only upgrade check")).addOption(new Option("--cli", "print an exact npm CLI upgrade command")).addOption(new Option("--image <reference>", "transactionally rebuild with an exact compatible image")).action((options) => action(async () => {
10129
+ program2.command("upgrade").description("Check CLI/image upgrades without claiming atomic self-upgrade").addOption(new Option("--check", "read-only upgrade check")).addOption(new Option("--cli", "print an exact npm CLI upgrade command")).addOption(new Option("--image <reference>", "transactionally rebuild with an exact semver/commit tag or digest")).action((options) => action(async () => {
9102
10130
  const selected = [options.check === true, options.cli === true, typeof options.image === "string"].filter(Boolean).length;
9103
10131
  if (selected > 1) throw new CliError("ARGUMENT_CONFLICT", "--check, --cli, and --image are mutually exclusive", EXIT.ARGUMENT);
9104
10132
  if (options.cli) {
@@ -9132,9 +10160,9 @@ for (const [oldName, replacement] of [["up", "start"], ["down", "stop"], ["updat
9132
10160
  }
9133
10161
  function sameEntrypoint(left, right) {
9134
10162
  try {
9135
- return fs4.realpathSync(left) === fs4.realpathSync(right);
10163
+ return fs5.realpathSync(left) === fs5.realpathSync(right);
9136
10164
  } catch {
9137
- return path4.resolve(left) === path4.resolve(right);
10165
+ return path5.resolve(left) === path5.resolve(right);
9138
10166
  }
9139
10167
  }
9140
10168
  if (process.argv[1] && sameEntrypoint(process.argv[1], fileURLToPath(import.meta.url))) {
@@ -9148,3 +10176,42 @@ if (process.argv[1] && sameEntrypoint(process.argv[1], fileURLToPath(import.meta
9148
10176
  process.exitCode = EXIT.SERVICE;
9149
10177
  });
9150
10178
  }
10179
+ /*! Bundled license information:
10180
+
10181
+ smol-toml/dist/error.js:
10182
+ smol-toml/dist/util.js:
10183
+ smol-toml/dist/date.js:
10184
+ smol-toml/dist/primitive.js:
10185
+ smol-toml/dist/extract.js:
10186
+ smol-toml/dist/struct.js:
10187
+ smol-toml/dist/parse.js:
10188
+ smol-toml/dist/stringify.js:
10189
+ smol-toml/dist/index.js:
10190
+ (*!
10191
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
10192
+ * SPDX-License-Identifier: BSD-3-Clause
10193
+ *
10194
+ * Redistribution and use in source and binary forms, with or without
10195
+ * modification, are permitted provided that the following conditions are met:
10196
+ *
10197
+ * 1. Redistributions of source code must retain the above copyright notice, this
10198
+ * list of conditions and the following disclaimer.
10199
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
10200
+ * this list of conditions and the following disclaimer in the
10201
+ * documentation and/or other materials provided with the distribution.
10202
+ * 3. Neither the name of the copyright holder nor the names of its contributors
10203
+ * may be used to endorse or promote products derived from this software without
10204
+ * specific prior written permission.
10205
+ *
10206
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
10207
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10208
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
10209
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
10210
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
10211
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
10212
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
10213
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
10214
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
10215
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
10216
+ *)
10217
+ */