fvtt-world-cli 1.0.0 → 1.1.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/dist/index.js CHANGED
@@ -10,7 +10,19 @@ import { readFileSync as readFileSync2 } from "node:fs";
10
10
  import { createHash as createHash2, randomBytes as randomBytes2, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
11
11
 
12
12
  // ../protocol/src/constants.js
13
- var PROTOCOL_VERSION = "3.0";
13
+ var PROTOCOL_VERSION = "1.1.1";
14
+ var PROTOCOL_COMPONENTS = Object.freeze({
15
+ MODULE: "module",
16
+ CLI_DAEMON: "cli-daemon",
17
+ UNKNOWN: "unknown"
18
+ });
19
+ var PROTOCOL_HANDSHAKES = Object.freeze({
20
+ CLI_DAEMON: "cli-daemon",
21
+ MODULE_DAEMON: "module-daemon",
22
+ COMMAND_REQUEST: "command-request",
23
+ DAEMON_REQUEST: "daemon-request",
24
+ UNKNOWN: "unknown"
25
+ });
14
26
  var MODULE_ID = "fvtt-world-cli";
15
27
  var DEFAULT_DAEMON_URL = "ws://127.0.0.1:47833";
16
28
  var AUTH_FIRST_MESSAGE_TIMEOUT_MS = 5e3;
@@ -26,6 +38,9 @@ var BRIDGE_TAKEOVER_CLOSE_CODE = 4001;
26
38
  var BRIDGE_TAKEOVER_CLOSE_REASON = "Bridge session taken over by the same pairing";
27
39
  var BRIDGE_RELEASE_CLOSE_CODE = 4002;
28
40
  var BRIDGE_RELEASE_CLOSE_REASON = "Bridge released";
41
+ var APPROVAL_AWAIT_PARK_CAP_MS = 25e3;
42
+ var APPROVAL_RESULT_RETENTION_MS = 5 * 60 * 1e3;
43
+ var POLICY_DISCOVERY_TIMEOUT_MS = 1500;
29
44
  var CLIENT_ID_MIN_LENGTH = 8;
30
45
  var CLIENT_ID_MAX_LENGTH = 64;
31
46
  var CLIENT_ID_PATTERN = "^[0-9a-fA-F-]+$";
@@ -87,6 +102,8 @@ var SCENE_THUMBNAIL_MAX_BYTES = 2 * 1024 * 1024;
87
102
  var SCENE_THUMBNAIL_RESPONSE_MAX_BYTES = 1024 * 1024;
88
103
  var TABLE_DRAW_MAX_COUNT = 100;
89
104
  var SETTING_VALUE_MAX_BYTES = 256 * 1024;
105
+ var MACRO_EXECUTE_TIMEOUT_DEFAULT_MS = 3e4;
106
+ var MACRO_EXECUTE_TIMEOUT_MAX_MS = 3e5;
90
107
  var SEARCH_MODES = Object.freeze(["name", "full"]);
91
108
  var SEARCH_SOURCES = Object.freeze(["world", "pack"]);
92
109
  var SEARCH_SNIPPET_FIELDS = Object.freeze(["text", "systemText", "name"]);
@@ -166,6 +183,7 @@ var ERROR_CODES = Object.freeze({
166
183
  COMPENDIUM_NOT_FOUND: "COMPENDIUM_NOT_FOUND",
167
184
  COMPENDIUM_ENTRY_NOT_FOUND: "COMPENDIUM_ENTRY_NOT_FOUND",
168
185
  IDEMPOTENCY_KEY_CONFLICT: "IDEMPOTENCY_KEY_CONFLICT",
186
+ IDEMPOTENCY_STORE_FULL: "IDEMPOTENCY_STORE_FULL",
169
187
  EFFECT_NOT_FOUND: "EFFECT_NOT_FOUND",
170
188
  PLAYLIST_NOT_FOUND: "PLAYLIST_NOT_FOUND",
171
189
  PLAYLIST_SOUND_NOT_FOUND: "PLAYLIST_SOUND_NOT_FOUND",
@@ -186,7 +204,11 @@ var ERROR_CODES = Object.freeze({
186
204
  SETTING_NOT_FOUND: "SETTING_NOT_FOUND",
187
205
  SETTING_READ_FAILED: "SETTING_READ_FAILED",
188
206
  SETTING_VALUE_NOT_SERIALIZABLE: "SETTING_VALUE_NOT_SERIALIZABLE",
207
+ SETTING_PROTECTED: "SETTING_PROTECTED",
208
+ SETTING_UNREGISTERED: "SETTING_UNREGISTERED",
209
+ MACRO_TIMEOUT: "MACRO_TIMEOUT",
189
210
  USER_NOT_FOUND: "USER_NOT_FOUND",
211
+ USER_SELF_PROTECTED: "USER_SELF_PROTECTED",
190
212
  SEARCH_INDEX_OVERFLOW: "SEARCH_INDEX_OVERFLOW",
191
213
  QUERY_TOO_BROAD: "QUERY_TOO_BROAD",
192
214
  UNSUPPORTED_OPERATION: "UNSUPPORTED_OPERATION",
@@ -199,11 +221,22 @@ var ERROR_CODES = Object.freeze({
199
221
  PAIRING_REQUIRED: "PAIRING_REQUIRED",
200
222
  PAIRING_NOT_FOUND: "PAIRING_NOT_FOUND",
201
223
  PAIRING_EXPIRED: "PAIRING_EXPIRED",
202
- BRIDGE_BUSY: "BRIDGE_BUSY"
224
+ BRIDGE_BUSY: "BRIDGE_BUSY",
225
+ COMMAND_DENIED: "COMMAND_DENIED",
226
+ APPROVAL_PENDING: "APPROVAL_PENDING",
227
+ APPROVAL_DENIED: "APPROVAL_DENIED",
228
+ APPROVAL_TIMEOUT: "APPROVAL_TIMEOUT",
229
+ APPROVAL_CANCELLED: "APPROVAL_CANCELLED",
230
+ APPROVAL_QUEUE_FULL: "APPROVAL_QUEUE_FULL",
231
+ APPROVAL_UNKNOWN: "APPROVAL_UNKNOWN"
203
232
  });
204
233
 
205
234
  // ../protocol/src/schemas/shared.js
206
- var cmd = (paramsSchema, { mutation = false } = {}) => ({ mutation, paramsSchema });
235
+ var cmd = (paramsSchema, { mutation = false, discovery = true } = {}) => ({
236
+ mutation,
237
+ ...discovery ? {} : { discovery: false },
238
+ paramsSchema
239
+ });
207
240
  function mergeCommandFamilies(families) {
208
241
  const merged = {};
209
242
  for (const family of families) {
@@ -282,6 +315,9 @@ var tokenIncludeProperty = {
282
315
  var compendiumIncludeProperty = {
283
316
  include: { type: "array", items: { type: "string", enum: ["effects"] } }
284
317
  };
318
+ var userIdsProperty = {
319
+ userIds: { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 }
320
+ };
285
321
  var idempotencyKeyProperty = {
286
322
  idempotencyKey: { type: "string", minLength: 1 }
287
323
  };
@@ -494,9 +530,96 @@ var userIdSchema = {
494
530
  properties: { userId: { type: "string", minLength: 1 } },
495
531
  additionalProperties: false
496
532
  };
533
+ var userRoleSchema = { type: "integer", enum: [1, 2, 3, 4] };
534
+ var userDataSchema = {
535
+ type: "object",
536
+ required: ["name"],
537
+ properties: {
538
+ name: { type: "string", minLength: 1 },
539
+ role: userRoleSchema,
540
+ color: { type: "string", minLength: 1 },
541
+ pronouns: { type: "string" },
542
+ avatar: nullableStringSchema,
543
+ character: nullableStringSchema,
544
+ flags: freeformObjectSchema
545
+ },
546
+ additionalProperties: false
547
+ };
548
+ var userPatchSchema = patchFrom(userDataSchema, { omit: ["role"] });
549
+ var userPermissionsSchema = {
550
+ type: "object",
551
+ required: [],
552
+ properties: {},
553
+ additionalProperties: { type: ["boolean", "null"] },
554
+ minProperties: 1
555
+ };
497
556
  var userCommands = {
498
557
  "user.list": cmd(nameFilteredListSchema),
499
- "user.get": cmd(userIdSchema)
558
+ "user.get": cmd(userIdSchema),
559
+ "user.create": cmd(
560
+ {
561
+ type: "object",
562
+ required: ["data"],
563
+ properties: {
564
+ data: userDataSchema,
565
+ ...dryRunProperty,
566
+ ...idempotencyKeyProperty
567
+ },
568
+ additionalProperties: false
569
+ },
570
+ { mutation: true }
571
+ ),
572
+ "user.update": cmd(
573
+ {
574
+ type: "object",
575
+ required: ["userId", "patch"],
576
+ properties: {
577
+ userId: { type: "string", minLength: 1 },
578
+ patch: userPatchSchema,
579
+ ...dryRunProperty
580
+ },
581
+ additionalProperties: false
582
+ },
583
+ { mutation: true }
584
+ ),
585
+ "user.delete": cmd(
586
+ {
587
+ type: "object",
588
+ required: ["userId"],
589
+ properties: {
590
+ userId: { type: "string", minLength: 1 },
591
+ ...dryRunProperty
592
+ },
593
+ additionalProperties: false
594
+ },
595
+ { mutation: true }
596
+ ),
597
+ "user.role.set": cmd(
598
+ {
599
+ type: "object",
600
+ required: ["userId", "role"],
601
+ properties: {
602
+ userId: { type: "string", minLength: 1 },
603
+ role: userRoleSchema,
604
+ ...dryRunProperty
605
+ },
606
+ additionalProperties: false
607
+ },
608
+ { mutation: true }
609
+ ),
610
+ "user.permissions.set": cmd(
611
+ {
612
+ type: "object",
613
+ required: ["userId", "permissions"],
614
+ properties: {
615
+ userId: { type: "string", minLength: 1 },
616
+ permissions: userPermissionsSchema,
617
+ ...dryRunProperty
618
+ },
619
+ additionalProperties: false
620
+ },
621
+ { mutation: true }
622
+ )
500
623
  };
501
624
 
502
625
  // ../protocol/src/schemas/actor.js
@@ -873,6 +996,34 @@ var actorCompendiumImportCommands = {
873
996
  "actor.import-from-compendium": cmd(compendiumImportSchema(actorPatchSchema), { mutation: true })
874
997
  };
875
998
 
999
+ // ../protocol/src/schemas/approval.js
1000
+ var approvalIdProperty = {
1001
+ approvalId: { type: "string", minLength: 22, maxLength: 22, pattern: "^[A-Za-z0-9_-]{22}$" }
1002
+ };
1003
+ var approvalAwaitSchema = {
1004
+ type: "object",
1005
+ required: ["approvalId"],
1006
+ properties: {
1007
+ ...approvalIdProperty,
1008
+ waitMs: { type: "integer", minimum: 0, maximum: APPROVAL_AWAIT_PARK_CAP_MS }
1009
+ },
1010
+ additionalProperties: false
1011
+ };
1012
+ var approvalCancelSchema = {
1013
+ type: "object",
1014
+ required: ["approvalId"],
1015
+ properties: { ...approvalIdProperty },
1016
+ additionalProperties: false
1017
+ };
1018
+ var APPROVAL_AWAIT_COMMAND = "approval.await";
1019
+ var APPROVAL_CANCEL_COMMAND = "approval.cancel";
1020
+ var POLICY_SNAPSHOT_COMMAND = "policy.snapshot";
1021
+ var approvalCommands = {
1022
+ [APPROVAL_AWAIT_COMMAND]: cmd(approvalAwaitSchema, { discovery: false }),
1023
+ [APPROVAL_CANCEL_COMMAND]: cmd(approvalCancelSchema, { discovery: false }),
1024
+ [POLICY_SNAPSHOT_COMMAND]: cmd(emptyObjectSchema, { discovery: false })
1025
+ };
1026
+
876
1027
  // ../protocol/src/schemas/cards.js
877
1028
  var cardBackSchema = {
878
1029
  type: "object",
@@ -1266,6 +1417,15 @@ var chatCommands = {
1266
1417
  additionalProperties: false
1267
1418
  },
1268
1419
  { mutation: true }
1420
+ ),
1421
+ "chat.flush": cmd(
1422
+ {
1423
+ type: "object",
1424
+ required: [],
1425
+ properties: { ...dryRunProperty },
1426
+ additionalProperties: false
1427
+ },
1428
+ { mutation: true }
1269
1429
  )
1270
1430
  };
1271
1431
 
@@ -1810,6 +1970,36 @@ var folderCommands = {
1810
1970
  "folder.delete": cmd(folderDeleteSchema, { mutation: true })
1811
1971
  };
1812
1972
 
1973
+ // ../protocol/src/schemas/game.js
1974
+ var gameCommands = {
1975
+ "game.pause": cmd(
1976
+ {
1977
+ type: "object",
1978
+ required: ["paused"],
1979
+ properties: {
1980
+ paused: { type: "boolean" },
1981
+ ...dryRunProperty
1982
+ },
1983
+ additionalProperties: false
1984
+ },
1985
+ { mutation: true }
1986
+ )
1987
+ };
1988
+
1989
+ // ../protocol/src/schemas/image.js
1990
+ var imageCommands = {
1991
+ "image.show": cmd({
1992
+ type: "object",
1993
+ required: ["src"],
1994
+ properties: {
1995
+ src: { type: "string", minLength: 1 },
1996
+ title: { type: "string", minLength: 1 },
1997
+ ...userIdsProperty
1998
+ },
1999
+ additionalProperties: false
2000
+ })
2001
+ };
2002
+
1813
2003
  // ../protocol/src/schemas/item.js
1814
2004
  var itemCommands = {
1815
2005
  "item.list": cmd(nameFilteredListSchema),
@@ -2193,6 +2383,16 @@ var journalCommands = {
2193
2383
  "journal.import-from-compendium": cmd(compendiumImportSchema(journalDocumentPatchSchema), {
2194
2384
  mutation: true
2195
2385
  }),
2386
+ "journal.show": cmd({
2387
+ type: "object",
2388
+ required: ["journalId"],
2389
+ properties: {
2390
+ journalId: { type: "string", minLength: 1 },
2391
+ force: { type: "boolean" },
2392
+ ...userIdsProperty
2393
+ },
2394
+ additionalProperties: false
2395
+ }),
2196
2396
  "journal.ownership.set": cmd(
2197
2397
  ownershipSetSchema("journalId", {
2198
2398
  levelSchema: journalOwnershipLevelSchema,
@@ -2277,6 +2477,22 @@ var macroDataSchema = {
2277
2477
  additionalProperties: false
2278
2478
  };
2279
2479
  var macroPatchSchema = patchFrom(macroDataSchema);
2480
+ var macroExecuteArgsSchema = {
2481
+ ...freeformObjectSchema,
2482
+ propertyNames: { pattern: "^[A-Za-z_$][A-Za-z0-9_$]*$" }
2483
+ };
2484
+ var macroExecuteScopeSchema = {
2485
+ type: "object",
2486
+ required: [],
2487
+ properties: {
2488
+ actorId: { type: "string", minLength: 1 },
2489
+ sceneId: { type: "string", minLength: 1 },
2490
+ tokenId: { type: "string", minLength: 1 },
2491
+ args: macroExecuteArgsSchema
2492
+ },
2493
+ additionalProperties: false,
2494
+ minProperties: 1
2495
+ };
2280
2496
  var macroIdSchema = {
2281
2497
  type: "object",
2282
2498
  required: ["macroId"],
@@ -2346,6 +2562,20 @@ var macroCommands = {
2346
2562
  },
2347
2563
  { mutation: true }
2348
2564
  ),
2565
+ "macro.execute": cmd(
2566
+ {
2567
+ type: "object",
2568
+ required: ["macroId"],
2569
+ properties: {
2570
+ macroId: { type: "string", minLength: 1 },
2571
+ scope: macroExecuteScopeSchema,
2572
+ timeoutMs: { type: "integer", minimum: 1, maximum: MACRO_EXECUTE_TIMEOUT_MAX_MS },
2573
+ ...dryRunProperty
2574
+ },
2575
+ additionalProperties: false
2576
+ },
2577
+ { mutation: true }
2578
+ ),
2349
2579
  "macro.import-from-compendium": cmd(compendiumImportSchema(macroPatchSchema), { mutation: true }),
2350
2580
  "macro.ownership.set": cmd(ownershipSetSchema("macroId", { levelSchema: ownershipLevelSchema }), {
2351
2581
  mutation: true
@@ -2902,6 +3132,26 @@ var regionBehaviorPatchSchema = {
2902
3132
  additionalProperties: true,
2903
3133
  minProperties: 1
2904
3134
  };
3135
+ var executableBehaviorTypeSchema = { type: "string", enum: ["executeMacro"] };
3136
+ var executableBehaviorCreateSchema = {
3137
+ type: "object",
3138
+ required: ["type"],
3139
+ properties: {
3140
+ type: executableBehaviorTypeSchema,
3141
+ ...regionBehaviorWriteProperties
3142
+ },
3143
+ additionalProperties: true
3144
+ };
3145
+ var executableBehaviorPatchSchema = {
3146
+ type: "object",
3147
+ required: [],
3148
+ properties: {
3149
+ type: executableBehaviorTypeSchema,
3150
+ ...regionBehaviorWriteProperties
3151
+ },
3152
+ additionalProperties: true,
3153
+ minProperties: 1
3154
+ };
2905
3155
  var sceneEmbeddedCommands = {
2906
3156
  "scene.token.list": cmd({
2907
3157
  type: "object",
@@ -3969,6 +4219,52 @@ var sceneEmbeddedCommands = {
3969
4219
  additionalProperties: false
3970
4220
  },
3971
4221
  { mutation: true }
4222
+ ),
4223
+ "scene.region.behavior.executable.create": cmd(
4224
+ {
4225
+ type: "object",
4226
+ required: ["sceneId", "regionId", "data"],
4227
+ properties: {
4228
+ sceneId: { type: "string", minLength: 1 },
4229
+ regionId: { type: "string", minLength: 1 },
4230
+ data: executableBehaviorCreateSchema,
4231
+ ...dryRunProperty,
4232
+ ...idempotencyKeyProperty
4233
+ },
4234
+ additionalProperties: false
4235
+ },
4236
+ { mutation: true }
4237
+ ),
4238
+ "scene.region.behavior.executable.update": cmd(
4239
+ {
4240
+ type: "object",
4241
+ required: ["sceneId", "regionId", "behaviorId", "patch"],
4242
+ properties: {
4243
+ sceneId: { type: "string", minLength: 1 },
4244
+ regionId: { type: "string", minLength: 1 },
4245
+ behaviorId: { type: "string", minLength: 1 },
4246
+ patch: executableBehaviorPatchSchema,
4247
+ ...dryRunProperty
4248
+ },
4249
+ additionalProperties: false
4250
+ },
4251
+ { mutation: true }
4252
+ ),
4253
+ "scene.region.behavior.executable.clone": cmd(
4254
+ {
4255
+ type: "object",
4256
+ required: ["sceneId", "regionId", "behaviorId"],
4257
+ properties: {
4258
+ sceneId: { type: "string", minLength: 1 },
4259
+ regionId: { type: "string", minLength: 1 },
4260
+ behaviorId: { type: "string", minLength: 1 },
4261
+ patch: executableBehaviorPatchSchema,
4262
+ ...dryRunProperty,
4263
+ ...idempotencyKeyProperty
4264
+ },
4265
+ additionalProperties: false
4266
+ },
4267
+ { mutation: true }
3972
4268
  )
3973
4269
  };
3974
4270
 
@@ -4150,6 +4446,27 @@ var sceneCommands = {
4150
4446
  },
4151
4447
  { mutation: true }
4152
4448
  ),
4449
+ "scene.activate": cmd(
4450
+ {
4451
+ type: "object",
4452
+ required: ["sceneId"],
4453
+ properties: {
4454
+ sceneId: { type: "string", minLength: 1 },
4455
+ ...dryRunProperty
4456
+ },
4457
+ additionalProperties: false
4458
+ },
4459
+ { mutation: true }
4460
+ ),
4461
+ "scene.pull-users": cmd({
4462
+ type: "object",
4463
+ required: ["sceneId"],
4464
+ properties: {
4465
+ sceneId: { type: "string", minLength: 1 },
4466
+ ...userIdsProperty
4467
+ },
4468
+ additionalProperties: false
4469
+ }),
4153
4470
  "scene.ownership.set": cmd(ownershipSetSchema("sceneId", { levelSchema: ownershipLevelSchema }), {
4154
4471
  mutation: true
4155
4472
  })
@@ -4181,24 +4498,70 @@ var settingListSchema = {
4181
4498
  properties: { ...nameFilterProperty, ...paginationProperties },
4182
4499
  additionalProperties: false
4183
4500
  };
4501
+ var settingKeyProperties = {
4502
+ namespace: { type: "string", minLength: 1 },
4503
+ key: { type: "string", minLength: 1 }
4504
+ };
4184
4505
  var settingGetSchema = {
4185
4506
  type: "object",
4186
4507
  required: ["namespace", "key"],
4508
+ properties: { ...settingKeyProperties },
4509
+ additionalProperties: false
4510
+ };
4511
+ var settingWriteItemSchema = {
4512
+ type: "object",
4513
+ required: ["namespace", "key", "value"],
4187
4514
  properties: {
4188
- namespace: { type: "string", minLength: 1 },
4189
- key: { type: "string", minLength: 1 }
4515
+ ...settingKeyProperties,
4516
+ value: {}
4190
4517
  },
4191
4518
  additionalProperties: false
4192
4519
  };
4193
4520
  var settingCommands = {
4194
4521
  "setting.list": cmd(settingListSchema),
4195
- "setting.get": cmd(settingGetSchema)
4522
+ "setting.get": cmd(settingGetSchema),
4523
+ "setting.get-many": cmd({
4524
+ type: "object",
4525
+ required: ["ids"],
4526
+ properties: { ...batchIdsProperty },
4527
+ additionalProperties: false
4528
+ }),
4529
+ "setting.set": cmd(
4530
+ {
4531
+ type: "object",
4532
+ required: ["namespace", "key", "value"],
4533
+ properties: {
4534
+ ...settingWriteItemSchema.properties,
4535
+ ...dryRunProperty
4536
+ },
4537
+ additionalProperties: false
4538
+ },
4539
+ { mutation: true }
4540
+ ),
4541
+ "setting.set-many": cmd(
4542
+ {
4543
+ type: "object",
4544
+ required: ["items"],
4545
+ properties: {
4546
+ items: {
4547
+ type: "array",
4548
+ minItems: 1,
4549
+ maxItems: BATCH_WRITE_MAX_ITEMS,
4550
+ items: settingWriteItemSchema
4551
+ },
4552
+ ...dryRunProperty
4553
+ },
4554
+ additionalProperties: false
4555
+ },
4556
+ { mutation: true }
4557
+ )
4196
4558
  };
4197
4559
 
4198
4560
  // ../protocol/src/schemas/system.js
4199
4561
  var systemCommands = {
4200
4562
  "system.ping": cmd(emptyObjectSchema),
4201
- "system.info": cmd(emptyObjectSchema)
4563
+ "system.info": cmd(emptyObjectSchema),
4564
+ "system.reload": cmd(emptyObjectSchema)
4202
4565
  };
4203
4566
 
4204
4567
  // ../protocol/src/schemas/table.js
@@ -4450,17 +4813,23 @@ var COMMAND_DEFINITIONS = deepFreeze(
4450
4813
  fileCommands,
4451
4814
  compendiumCommands,
4452
4815
  folderCommands,
4816
+ gameCommands,
4817
+ imageCommands,
4453
4818
  userCommands,
4454
4819
  settingCommands,
4455
4820
  actorCompendiumImportCommands,
4456
4821
  worldAuditCommands,
4457
- worldSearchCommands
4822
+ worldSearchCommands,
4823
+ approvalCommands
4458
4824
  ])
4459
4825
  );
4460
4826
  var COMMAND_NAMES = deepFreeze(Object.keys(COMMAND_DEFINITIONS));
4461
4827
  var WRITE_COMMANDS = deepFreeze(
4462
4828
  COMMAND_NAMES.filter((command) => COMMAND_DEFINITIONS[command].mutation === true)
4463
4829
  );
4830
+ var DISCOVERABLE_COMMAND_NAMES = deepFreeze(
4831
+ COMMAND_NAMES.filter((command) => COMMAND_DEFINITIONS[command].discovery !== false)
4832
+ );
4464
4833
  var REQUEST_SCHEMA = {
4465
4834
  type: "object",
4466
4835
  required: ["protocolVersion", "type", "id", "command", "params"],
@@ -4808,6 +5177,370 @@ function getInvalidCommandError(command) {
4808
5177
  };
4809
5178
  }
4810
5179
 
5180
+ // ../protocol/src/generated/default-command-profile.js
5181
+ var DEFAULT_COMMAND_PROFILE = deepFreeze({
5182
+ "system.ping": "allow",
5183
+ "system.info": "allow",
5184
+ "system.reload": "approve",
5185
+ "scene.list": "allow",
5186
+ "scene.get": "allow",
5187
+ "scene.get-many": "allow",
5188
+ "scene.update": "allow",
5189
+ "scene.create": "allow",
5190
+ "scene.delete": "approve",
5191
+ "scene.clone": "allow",
5192
+ "scene.import-from-compendium": "allow",
5193
+ "scene.thumbnail.generate": "allow",
5194
+ "scene.fog.reset": "approve",
5195
+ "scene.activate": "allow",
5196
+ "scene.pull-users": "allow",
5197
+ "scene.ownership.set": "allow",
5198
+ "scene.token.list": "allow",
5199
+ "scene.token.get": "allow",
5200
+ "scene.token.create": "allow",
5201
+ "scene.token.update": "allow",
5202
+ "scene.token.delete": "approve",
5203
+ "scene.token.clone": "allow",
5204
+ "scene.token.create-many": "allow",
5205
+ "scene.token.update-many": "allow",
5206
+ "scene.token.delete-many": "approve",
5207
+ "scene.token.item.list": "allow",
5208
+ "scene.token.item.get": "allow",
5209
+ "scene.token.item.create": "allow",
5210
+ "scene.token.item.update": "allow",
5211
+ "scene.token.item.delete": "approve",
5212
+ "scene.token.item.clone": "allow",
5213
+ "scene.token.item.effect.list": "allow",
5214
+ "scene.token.item.effect.get": "allow",
5215
+ "scene.token.item.effect.create": "allow",
5216
+ "scene.token.item.effect.update": "allow",
5217
+ "scene.token.item.effect.delete": "approve",
5218
+ "scene.token.item.effect.clone": "allow",
5219
+ "scene.token.item.effect.create-many": "allow",
5220
+ "scene.token.item.effect.update-many": "allow",
5221
+ "scene.token.item.effect.delete-many": "approve",
5222
+ "scene.token.effect.list": "allow",
5223
+ "scene.token.effect.applied": "allow",
5224
+ "scene.token.effect.get": "allow",
5225
+ "scene.token.effect.create": "allow",
5226
+ "scene.token.effect.update": "allow",
5227
+ "scene.token.effect.delete": "approve",
5228
+ "scene.token.effect.clone": "allow",
5229
+ "scene.token.effect.create-many": "allow",
5230
+ "scene.token.effect.update-many": "allow",
5231
+ "scene.token.effect.delete-many": "approve",
5232
+ "scene.tile.list": "allow",
5233
+ "scene.tile.get": "allow",
5234
+ "scene.tile.create": "allow",
5235
+ "scene.tile.update": "allow",
5236
+ "scene.tile.delete": "approve",
5237
+ "scene.tile.clone": "allow",
5238
+ "scene.tile.create-many": "allow",
5239
+ "scene.tile.update-many": "allow",
5240
+ "scene.tile.delete-many": "approve",
5241
+ "scene.sound.list": "allow",
5242
+ "scene.sound.get": "allow",
5243
+ "scene.sound.create": "allow",
5244
+ "scene.sound.update": "allow",
5245
+ "scene.sound.delete": "approve",
5246
+ "scene.sound.clone": "allow",
5247
+ "scene.sound.create-many": "allow",
5248
+ "scene.sound.update-many": "allow",
5249
+ "scene.sound.delete-many": "approve",
5250
+ "scene.wall.list": "allow",
5251
+ "scene.wall.get": "allow",
5252
+ "scene.wall.create": "allow",
5253
+ "scene.wall.update": "allow",
5254
+ "scene.wall.delete": "approve",
5255
+ "scene.wall.clone": "allow",
5256
+ "scene.wall.create-many": "allow",
5257
+ "scene.wall.update-many": "allow",
5258
+ "scene.wall.delete-many": "approve",
5259
+ "scene.note.list": "allow",
5260
+ "scene.note.get": "allow",
5261
+ "scene.note.create": "allow",
5262
+ "scene.note.update": "allow",
5263
+ "scene.note.delete": "approve",
5264
+ "scene.note.clone": "allow",
5265
+ "scene.note.create-many": "allow",
5266
+ "scene.note.update-many": "allow",
5267
+ "scene.note.delete-many": "approve",
5268
+ "scene.drawing.list": "allow",
5269
+ "scene.drawing.get": "allow",
5270
+ "scene.drawing.create": "allow",
5271
+ "scene.drawing.update": "allow",
5272
+ "scene.drawing.delete": "approve",
5273
+ "scene.drawing.clone": "allow",
5274
+ "scene.drawing.create-many": "allow",
5275
+ "scene.drawing.update-many": "allow",
5276
+ "scene.drawing.delete-many": "approve",
5277
+ "scene.light.list": "allow",
5278
+ "scene.light.get": "allow",
5279
+ "scene.light.create": "allow",
5280
+ "scene.light.update": "allow",
5281
+ "scene.light.delete": "approve",
5282
+ "scene.light.clone": "allow",
5283
+ "scene.light.create-many": "allow",
5284
+ "scene.light.update-many": "allow",
5285
+ "scene.light.delete-many": "approve",
5286
+ "scene.template.list": "allow",
5287
+ "scene.template.get": "allow",
5288
+ "scene.template.create": "allow",
5289
+ "scene.template.update": "allow",
5290
+ "scene.template.delete": "approve",
5291
+ "scene.template.clone": "allow",
5292
+ "scene.template.create-many": "allow",
5293
+ "scene.template.update-many": "allow",
5294
+ "scene.template.delete-many": "approve",
5295
+ "scene.region.list": "allow",
5296
+ "scene.region.get": "allow",
5297
+ "scene.region.create": "allow",
5298
+ "scene.region.update": "allow",
5299
+ "scene.region.delete": "approve",
5300
+ "scene.region.clone": "allow",
5301
+ "scene.region.create-many": "allow",
5302
+ "scene.region.update-many": "allow",
5303
+ "scene.region.delete-many": "approve",
5304
+ "scene.region.behavior.list": "allow",
5305
+ "scene.region.behavior.get": "allow",
5306
+ "scene.region.behavior.create": "allow",
5307
+ "scene.region.behavior.update": "allow",
5308
+ "scene.region.behavior.delete": "approve",
5309
+ "scene.region.behavior.clone": "allow",
5310
+ "scene.region.behavior.executable.create": "deny",
5311
+ "scene.region.behavior.executable.update": "deny",
5312
+ "scene.region.behavior.executable.clone": "deny",
5313
+ "item.list": "allow",
5314
+ "item.get": "allow",
5315
+ "item.get-many": "allow",
5316
+ "item.create": "allow",
5317
+ "item.update": "allow",
5318
+ "item.delete": "approve",
5319
+ "item.clone": "allow",
5320
+ "item.import-from-compendium": "allow",
5321
+ "item.ownership.set": "allow",
5322
+ "item.update-many": "allow",
5323
+ "item.delete-many": "approve",
5324
+ "item.effect.list": "allow",
5325
+ "item.effect.get": "allow",
5326
+ "item.effect.create": "allow",
5327
+ "item.effect.update": "allow",
5328
+ "item.effect.delete": "approve",
5329
+ "item.effect.clone": "allow",
5330
+ "item.effect.create-many": "allow",
5331
+ "item.effect.update-many": "allow",
5332
+ "item.effect.delete-many": "approve",
5333
+ "journal.list": "allow",
5334
+ "journal.get": "allow",
5335
+ "journal.get-many": "allow",
5336
+ "journal.create": "allow",
5337
+ "journal.update": "allow",
5338
+ "journal.delete": "approve",
5339
+ "journal.clone": "allow",
5340
+ "journal.import-from-compendium": "allow",
5341
+ "journal.show": "allow",
5342
+ "journal.ownership.set": "allow",
5343
+ "journal.update-many": "allow",
5344
+ "journal.delete-many": "approve",
5345
+ "journal.category.list": "allow",
5346
+ "journal.category.get": "allow",
5347
+ "journal.category.create": "allow",
5348
+ "journal.category.update": "allow",
5349
+ "journal.category.delete": "approve",
5350
+ "macro.list": "allow",
5351
+ "macro.get": "allow",
5352
+ "macro.get-many": "allow",
5353
+ "macro.create": "allow",
5354
+ "macro.update": "allow",
5355
+ "macro.delete": "approve",
5356
+ "macro.clone": "allow",
5357
+ "macro.execute": "deny",
5358
+ "macro.import-from-compendium": "allow",
5359
+ "macro.ownership.set": "allow",
5360
+ "playlist.list": "allow",
5361
+ "playlist.get": "allow",
5362
+ "playlist.get-many": "allow",
5363
+ "playlist.create": "allow",
5364
+ "playlist.update": "allow",
5365
+ "playlist.delete": "approve",
5366
+ "playlist.clone": "allow",
5367
+ "playlist.import-from-compendium": "allow",
5368
+ "playlist.ownership.set": "allow",
5369
+ "playlist.sound.list": "allow",
5370
+ "playlist.sound.get": "allow",
5371
+ "playlist.sound.create": "allow",
5372
+ "playlist.sound.update": "allow",
5373
+ "playlist.sound.delete": "approve",
5374
+ "playlist.sound.clone": "allow",
5375
+ "playlist.play": "allow",
5376
+ "playlist.stop": "allow",
5377
+ "playlist.playNext": "allow",
5378
+ "playlist.sound.play": "allow",
5379
+ "playlist.sound.stop": "allow",
5380
+ "table.list": "allow",
5381
+ "table.get": "allow",
5382
+ "table.get-many": "allow",
5383
+ "table.create": "allow",
5384
+ "table.update": "allow",
5385
+ "table.delete": "approve",
5386
+ "table.clone": "allow",
5387
+ "table.import-from-compendium": "allow",
5388
+ "table.draw": "allow",
5389
+ "table.reset": "allow",
5390
+ "table.ownership.set": "allow",
5391
+ "table.result.list": "allow",
5392
+ "table.result.get": "allow",
5393
+ "table.result.create": "allow",
5394
+ "table.result.update": "allow",
5395
+ "table.result.delete": "approve",
5396
+ "table.result.clone": "allow",
5397
+ "combat.list": "allow",
5398
+ "combat.get": "allow",
5399
+ "combat.create": "allow",
5400
+ "combat.update": "allow",
5401
+ "combat.delete": "approve",
5402
+ "combat.start": "allow",
5403
+ "combat.activate": "allow",
5404
+ "combat.next-turn": "allow",
5405
+ "combat.previous-turn": "allow",
5406
+ "combat.next-round": "allow",
5407
+ "combat.previous-round": "allow",
5408
+ "combat.reset-initiative": "allow",
5409
+ "combat.roll-initiative": "allow",
5410
+ "combat.set-initiative": "allow",
5411
+ "combat.combatant.list": "allow",
5412
+ "combat.combatant.get": "allow",
5413
+ "combat.combatant.create": "allow",
5414
+ "combat.combatant.update": "allow",
5415
+ "combat.combatant.delete": "approve",
5416
+ "combat.group.list": "allow",
5417
+ "combat.group.get": "allow",
5418
+ "combat.group.create": "allow",
5419
+ "combat.group.update": "allow",
5420
+ "combat.group.delete": "approve",
5421
+ "cards.list": "allow",
5422
+ "cards.get": "allow",
5423
+ "cards.get-many": "allow",
5424
+ "cards.create": "allow",
5425
+ "cards.update": "allow",
5426
+ "cards.clone": "allow",
5427
+ "cards.import-from-compendium": "allow",
5428
+ "cards.delete": "approve",
5429
+ "cards.ownership.set": "allow",
5430
+ "cards.shuffle": "allow",
5431
+ "cards.reset": "allow",
5432
+ "cards.deal": "allow",
5433
+ "cards.draw": "allow",
5434
+ "cards.pass": "allow",
5435
+ "cards.card.list": "allow",
5436
+ "cards.card.get": "allow",
5437
+ "cards.card.create": "allow",
5438
+ "cards.card.update": "allow",
5439
+ "cards.card.clone": "allow",
5440
+ "cards.card.delete": "approve",
5441
+ "chat.list": "allow",
5442
+ "chat.get": "allow",
5443
+ "chat.create": "allow",
5444
+ "chat.delete": "approve",
5445
+ "chat.flush": "approve",
5446
+ "actor.list": "allow",
5447
+ "actor.get": "allow",
5448
+ "actor.get-many": "allow",
5449
+ "actor.create": "allow",
5450
+ "actor.update": "allow",
5451
+ "actor.delete": "approve",
5452
+ "actor.clone": "allow",
5453
+ "actor.ownership.set": "allow",
5454
+ "actor.item.list": "allow",
5455
+ "actor.item.create": "allow",
5456
+ "actor.item.update": "allow",
5457
+ "actor.item.get": "allow",
5458
+ "actor.item.delete": "approve",
5459
+ "actor.item.clone": "allow",
5460
+ "actor.item.import-from-compendium": "allow",
5461
+ "actor.update-many": "allow",
5462
+ "actor.delete-many": "approve",
5463
+ "actor.effect.list": "allow",
5464
+ "actor.effect.applied": "allow",
5465
+ "actor.effect.get": "allow",
5466
+ "actor.effect.create": "allow",
5467
+ "actor.effect.update": "allow",
5468
+ "actor.effect.delete": "approve",
5469
+ "actor.effect.clone": "allow",
5470
+ "actor.effect.create-many": "allow",
5471
+ "actor.effect.update-many": "allow",
5472
+ "actor.effect.delete-many": "approve",
5473
+ "actor.item.effect.list": "allow",
5474
+ "actor.item.effect.get": "allow",
5475
+ "actor.item.effect.create": "allow",
5476
+ "actor.item.effect.update": "allow",
5477
+ "actor.item.effect.delete": "approve",
5478
+ "actor.item.effect.clone": "allow",
5479
+ "actor.item.effect.create-many": "allow",
5480
+ "actor.item.effect.update-many": "allow",
5481
+ "actor.item.effect.delete-many": "approve",
5482
+ "file.list": "allow",
5483
+ "file.stat": "allow",
5484
+ "file.read": "allow",
5485
+ "file.mkdir": "allow",
5486
+ "file.upload": "allow",
5487
+ "file.delete": "approve",
5488
+ "file.move": "approve",
5489
+ "compendium.list": "allow",
5490
+ "compendium.index": "allow",
5491
+ "compendium.get": "allow",
5492
+ "folder.list": "allow",
5493
+ "folder.get": "allow",
5494
+ "folder.create": "allow",
5495
+ "folder.update": "allow",
5496
+ "folder.delete": "approve",
5497
+ "game.pause": "allow",
5498
+ "image.show": "allow",
5499
+ "user.list": "allow",
5500
+ "user.get": "allow",
5501
+ "user.create": "approve",
5502
+ "user.update": "allow",
5503
+ "user.delete": "approve",
5504
+ "user.role.set": "deny",
5505
+ "user.permissions.set": "deny",
5506
+ "setting.list": "allow",
5507
+ "setting.get": "allow",
5508
+ "setting.get-many": "allow",
5509
+ "setting.set": "deny",
5510
+ "setting.set-many": "deny",
5511
+ "actor.import-from-compendium": "allow",
5512
+ "world.audit-files": "allow",
5513
+ "world.search": "allow",
5514
+ "approval.await": "allow",
5515
+ "approval.cancel": "allow",
5516
+ "policy.snapshot": "allow"
5517
+ });
5518
+
5519
+ // ../protocol/src/command-risk.js
5520
+ var DESTRUCTIVE_VERBS = Object.freeze(["delete", "delete-many"]);
5521
+ var DESTRUCTIVE_COMMANDS = Object.freeze(["chat.flush", "file.move", "scene.fog.reset"]);
5522
+ var APPROVE_EXTRA_COMMANDS = Object.freeze(["system.reload", "user.create"]);
5523
+ var DENIED_BY_DEFAULT_COMMANDS = Object.freeze([
5524
+ "macro.execute",
5525
+ "setting.set",
5526
+ "setting.set-many",
5527
+ "user.role.set",
5528
+ "user.permissions.set",
5529
+ "scene.region.behavior.executable.create",
5530
+ "scene.region.behavior.executable.update",
5531
+ "scene.region.behavior.executable.clone"
5532
+ ]);
5533
+
5534
+ // ../protocol/src/policy.js
5535
+ var POLICY_BEHAVIORS = Object.freeze(["allow", "approve", "deny"]);
5536
+ var POLICY_EXEMPT_COMMANDS = Object.freeze([
5537
+ "system.ping",
5538
+ "system.info",
5539
+ "approval.await",
5540
+ "approval.cancel",
5541
+ "policy.snapshot"
5542
+ ]);
5543
+
4811
5544
  // ../protocol/src/validation.js
4812
5545
  function isPlainObject(value) {
4813
5546
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -4906,6 +5639,18 @@ function validateObjectSchema(schema, value, path, errors) {
4906
5639
  if (typeof schema.minProperties === "number" && ownKeys.length < schema.minProperties) {
4907
5640
  errors.push(`${path} must contain at least ${schema.minProperties} properties`);
4908
5641
  }
5642
+ if (isPlainObject(schema.propertyNames) && typeof schema.propertyNames.pattern === "string") {
5643
+ const namePattern = compilePattern(schema.propertyNames.pattern);
5644
+ for (const key of ownKeys) {
5645
+ if (namePattern === null) {
5646
+ errors.push(
5647
+ `${path}.${key} cannot be validated: ${schema.propertyNames.pattern} is not a valid unicode-mode pattern`
5648
+ );
5649
+ } else if (!namePattern.test(key)) {
5650
+ errors.push(`${path}.${key} is not an allowed property name`);
5651
+ }
5652
+ }
5653
+ }
4909
5654
  for (const [key, propertyValue] of Object.entries(value)) {
4910
5655
  const propertySchema = properties[key];
4911
5656
  if (propertySchema) {
@@ -5106,13 +5851,68 @@ function createProtocolError({ code, message, details = {} }) {
5106
5851
  details
5107
5852
  };
5108
5853
  }
5109
- function getProtocolVersionError(actualVersion) {
5854
+ var LEGACY_PROTOCOL_RELEASES = Object.freeze({ "3.0": "1.0.0" });
5855
+ var RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
5856
+ function normalizeComparableProtocolVersion(value) {
5857
+ if (typeof value !== "string") {
5858
+ return null;
5859
+ }
5860
+ if (Object.hasOwn(LEGACY_PROTOCOL_RELEASES, value)) {
5861
+ return LEGACY_PROTOCOL_RELEASES[value];
5862
+ }
5863
+ return RELEASE_VERSION_PATTERN.test(value) ? value : null;
5864
+ }
5865
+ function compareReleaseVersions(left, right) {
5866
+ const leftParts = left.split(".").map(Number);
5867
+ const rightParts = right.split(".").map(Number);
5868
+ for (let index = 0; index < leftParts.length; index += 1) {
5869
+ if (leftParts[index] !== rightParts[index]) {
5870
+ return leftParts[index] < rightParts[index] ? -1 : 1;
5871
+ }
5872
+ }
5873
+ return 0;
5874
+ }
5875
+ function isNamedComponent(component) {
5876
+ return component === PROTOCOL_COMPONENTS.MODULE || component === PROTOCOL_COMPONENTS.CLI_DAEMON;
5877
+ }
5878
+ function resolveStaleness(actualVersion, peer, reporter) {
5879
+ const actual = normalizeComparableProtocolVersion(actualVersion);
5880
+ const expected = normalizeComparableProtocolVersion(PROTOCOL_VERSION);
5881
+ if (actual === null || expected === null) {
5882
+ return { staleComponent: PROTOCOL_COMPONENTS.UNKNOWN, comparable: false };
5883
+ }
5884
+ const order = compareReleaseVersions(actual, expected);
5885
+ const older = order < 0 ? peer : order > 0 ? reporter : PROTOCOL_COMPONENTS.UNKNOWN;
5886
+ return {
5887
+ staleComponent: isNamedComponent(older) ? older : PROTOCOL_COMPONENTS.UNKNOWN,
5888
+ comparable: true
5889
+ };
5890
+ }
5891
+ function describeStaleness({ staleComponent, comparable }) {
5892
+ if (staleComponent === PROTOCOL_COMPONENTS.MODULE) {
5893
+ return "the Foundry module is the older component, so update the module in Foundry until both halves come from the same release, then reload the GM client";
5894
+ }
5895
+ if (staleComponent === PROTOCOL_COMPONENTS.CLI_DAEMON) {
5896
+ return "the CLI and daemon are the older component, so update the fvtt-world-cli package until both halves come from the same release, then restart the daemon";
5897
+ }
5898
+ const cause = comparable ? "the older component is not identified here" : "these versions cannot be ordered, so the older component is unknown";
5899
+ return `${cause}: bring the fvtt-world-cli package and the Foundry module to the same release, restart the daemon, and reload the GM client`;
5900
+ }
5901
+ function getProtocolVersionError(actualVersion, options = {}) {
5902
+ const {
5903
+ peer = PROTOCOL_COMPONENTS.UNKNOWN,
5904
+ reporter = PROTOCOL_COMPONENTS.UNKNOWN,
5905
+ handshake = PROTOCOL_HANDSHAKES.UNKNOWN
5906
+ } = options;
5907
+ const staleness = resolveStaleness(actualVersion, peer, reporter);
5110
5908
  return createProtocolError({
5111
5909
  code: ERROR_CODES.UNSUPPORTED_PROTOCOL_VERSION,
5112
- message: `Unsupported protocol version: ${actualVersion}`,
5910
+ message: `Unsupported protocol version: ${actualVersion} (expected ${PROTOCOL_VERSION}); ${describeStaleness(staleness)}. Components from different releases are refused by design.`,
5113
5911
  details: {
5114
5912
  expectedVersion: PROTOCOL_VERSION,
5115
- actualVersion
5913
+ actualVersion,
5914
+ staleComponent: staleness.staleComponent,
5915
+ handshake
5116
5916
  }
5117
5917
  });
5118
5918
  }
@@ -5345,6 +6145,26 @@ function resolveBridgeServeConfig({
5345
6145
  }
5346
6146
 
5347
6147
  // src/transport-util.ts
6148
+ var APPROVAL_ID_PATTERN = /^[A-Za-z0-9_-]{22}$/;
6149
+ var MAX_EPOCH_MS = 864e13;
6150
+ function readApprovalId(value) {
6151
+ return typeof value === "string" && APPROVAL_ID_PATTERN.test(value) ? value : null;
6152
+ }
6153
+ function readPendingApprovalDetails(envelope) {
6154
+ if (envelope.ok || envelope.error?.code !== ERROR_CODES.APPROVAL_PENDING) {
6155
+ return null;
6156
+ }
6157
+ const details = envelope.error.details ?? {};
6158
+ const approvalId = readApprovalId(details.approvalId);
6159
+ const expiresAt = details.expiresAt;
6160
+ if (approvalId === null || typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) {
6161
+ return null;
6162
+ }
6163
+ if (Math.abs(expiresAt) > MAX_EPOCH_MS) {
6164
+ return null;
6165
+ }
6166
+ return { approvalId, expiresAt };
6167
+ }
5348
6168
  var DaemonTransportError = class extends Error {
5349
6169
  code;
5350
6170
  details;
@@ -5370,6 +6190,21 @@ function normalizeIncomingData(data) {
5370
6190
  function isCommandResponseEnvelope(message, acceptedTypes) {
5371
6191
  return typeof message === "object" && message !== null && acceptedTypes.includes(message.type) && typeof message.id === "string" && typeof message.ok === "boolean";
5372
6192
  }
6193
+ function protocolVersionSkewError(message) {
6194
+ if (typeof message !== "object" || message === null) {
6195
+ return null;
6196
+ }
6197
+ const frame = message;
6198
+ if (frame.type !== MESSAGE_TYPES.CLIENT_HELLO_ACK || typeof frame.protocolVersion !== "string" || frame.protocolVersion === PROTOCOL_VERSION) {
6199
+ return null;
6200
+ }
6201
+ const error = getProtocolVersionError(frame.protocolVersion, {
6202
+ peer: PROTOCOL_COMPONENTS.CLI_DAEMON,
6203
+ reporter: PROTOCOL_COMPONENTS.CLI_DAEMON,
6204
+ handshake: PROTOCOL_HANDSHAKES.CLI_DAEMON
6205
+ });
6206
+ return new DaemonTransportError(error.code, error.message, error.details);
6207
+ }
5373
6208
  function decodeTransportFrame(data) {
5374
6209
  const parsed = parseBridgeMessage(normalizeIncomingData(data));
5375
6210
  if (!parsed.ok) {
@@ -5382,6 +6217,10 @@ function decodeTransportFrame(data) {
5382
6217
  )
5383
6218
  };
5384
6219
  }
6220
+ const skew = protocolVersionSkewError(parsed.value);
6221
+ if (skew) {
6222
+ return { ok: false, error: skew };
6223
+ }
5385
6224
  const validation = validateTransportMessage(parsed.value);
5386
6225
  if (!validation.ok) {
5387
6226
  return {
@@ -5402,10 +6241,195 @@ function sendJson(socket, payload) {
5402
6241
  socket.send(JSON.stringify(payload));
5403
6242
  }
5404
6243
 
6244
+ // src/bridge/approval-link.ts
6245
+ var DEFAULT_MAX_ENTRIES = 1e3;
6246
+ function settlementForAwait(envelope) {
6247
+ if (!envelope.ok) {
6248
+ const approvalId2 = readApprovalId(envelope.error?.details?.approvalId);
6249
+ return envelope.error?.code === ERROR_CODES.APPROVAL_UNKNOWN && approvalId2 ? { kind: "indeterminate", approvalId: approvalId2 } : { kind: "none" };
6250
+ }
6251
+ const report = envelope.result ?? {};
6252
+ const approvalId = readApprovalId(report.approvalId);
6253
+ if (!approvalId || report.status !== "resolved") {
6254
+ return { kind: "none" };
6255
+ }
6256
+ if (report.outcome !== "approved") {
6257
+ return { kind: "clear", approvalId };
6258
+ }
6259
+ const response = report.response;
6260
+ return isCommandResponseEnvelope(response, [MESSAGE_TYPES.COMMAND_RESPONSE]) ? { kind: "promote", approvalId, response } : { kind: "indeterminate", approvalId };
6261
+ }
6262
+ function settlementForCancel(envelope) {
6263
+ if (!envelope.ok) {
6264
+ return { kind: "none" };
6265
+ }
6266
+ const report = envelope.result ?? {};
6267
+ const approvalId = readApprovalId(report.approvalId);
6268
+ if (!approvalId) {
6269
+ return { kind: "none" };
6270
+ }
6271
+ if (report.status === "cancelled") {
6272
+ return { kind: "clear", approvalId };
6273
+ }
6274
+ return report.status === "resolved" ? { kind: "none" } : { kind: "indeterminate", approvalId };
6275
+ }
6276
+ function readApprovalSettlement(command, envelope) {
6277
+ if (command === APPROVAL_AWAIT_COMMAND) {
6278
+ return settlementForAwait(envelope);
6279
+ }
6280
+ if (command === APPROVAL_CANCEL_COMMAND) {
6281
+ return settlementForCancel(envelope);
6282
+ }
6283
+ return { kind: "none" };
6284
+ }
6285
+ var ApprovalIdempotencyLinks = class {
6286
+ maxEntries;
6287
+ retentionMs;
6288
+ now;
6289
+ links;
6290
+ keysByApprovalId;
6291
+ constructor({
6292
+ maxEntries = DEFAULT_MAX_ENTRIES,
6293
+ retentionMs = APPROVAL_RESULT_RETENTION_MS,
6294
+ now = () => Date.now()
6295
+ } = {}) {
6296
+ this.maxEntries = maxEntries;
6297
+ this.retentionMs = retentionMs;
6298
+ this.now = now;
6299
+ this.links = /* @__PURE__ */ new Map();
6300
+ this.keysByApprovalId = /* @__PURE__ */ new Map();
6301
+ }
6302
+ record({
6303
+ key,
6304
+ fingerprint,
6305
+ approvalId,
6306
+ expiresAt,
6307
+ pendingResponse
6308
+ }) {
6309
+ this.prune();
6310
+ this.forget(key);
6311
+ this.links.set(key, {
6312
+ key,
6313
+ kind: "link",
6314
+ fingerprint,
6315
+ approvalId,
6316
+ pendingResponse,
6317
+ expiresAt: expiresAt + this.retentionMs
6318
+ });
6319
+ this.keysByApprovalId.set(approvalId, key);
6320
+ }
6321
+ // The reservation is held for the pending request's whole lifetime, so a tombstone converts an
6322
+ // existing entry instead of competing for capacity with newer keys. recordLostInFlight stays
6323
+ // unconditional so that the boundary tick, where both expire at once, still retains the key.
6324
+ reserve({
6325
+ key,
6326
+ fingerprint,
6327
+ retainMs
6328
+ }) {
6329
+ this.prune();
6330
+ const existing = this.links.get(key);
6331
+ if (existing) {
6332
+ return existing.kind === "reserved" && existing.fingerprint === fingerprint ? { ok: true } : { ok: false, reason: "conflict" };
6333
+ }
6334
+ if (this.links.size >= this.maxEntries) {
6335
+ return { ok: false, reason: "full" };
6336
+ }
6337
+ this.links.set(key, {
6338
+ key,
6339
+ kind: "reserved",
6340
+ fingerprint,
6341
+ approvalId: null,
6342
+ pendingResponse: null,
6343
+ expiresAt: this.now() + retainMs
6344
+ });
6345
+ return { ok: true };
6346
+ }
6347
+ release(key) {
6348
+ if (this.links.get(key)?.kind === "reserved") {
6349
+ this.forget(key);
6350
+ }
6351
+ }
6352
+ recordLostInFlight({ key, fingerprint, retainMs }) {
6353
+ this.prune();
6354
+ const existing = this.links.get(key);
6355
+ if (existing && existing.kind !== "reserved") {
6356
+ return false;
6357
+ }
6358
+ this.links.set(key, {
6359
+ key,
6360
+ kind: "lost",
6361
+ fingerprint,
6362
+ approvalId: null,
6363
+ pendingResponse: null,
6364
+ expiresAt: this.now() + retainMs
6365
+ });
6366
+ return true;
6367
+ }
6368
+ lookup(key, fingerprint) {
6369
+ this.prune();
6370
+ const link = this.links.get(key);
6371
+ if (!link) {
6372
+ return { status: "miss" };
6373
+ }
6374
+ if (link.kind === "reserved") {
6375
+ return { status: "miss" };
6376
+ }
6377
+ if (link.fingerprint !== fingerprint) {
6378
+ return { status: "conflict", approvalId: link.approvalId };
6379
+ }
6380
+ if (link.approvalId === null) {
6381
+ return { status: "lost-in-flight" };
6382
+ }
6383
+ return link.pendingResponse ? { status: "pending", approvalId: link.approvalId, response: link.pendingResponse } : { status: "indeterminate", approvalId: link.approvalId };
6384
+ }
6385
+ settle(settlement) {
6386
+ if (settlement.kind === "none") {
6387
+ return null;
6388
+ }
6389
+ const key = this.keysByApprovalId.get(settlement.approvalId);
6390
+ const link = key === void 0 ? void 0 : this.links.get(key);
6391
+ if (!link) {
6392
+ return null;
6393
+ }
6394
+ if (settlement.kind === "indeterminate") {
6395
+ link.pendingResponse = null;
6396
+ return null;
6397
+ }
6398
+ const promoted = settlement.kind === "promote" ? { key: link.key, fingerprint: link.fingerprint } : null;
6399
+ this.forget(link.key);
6400
+ return promoted;
6401
+ }
6402
+ prune() {
6403
+ const now = this.now();
6404
+ for (const [key, link] of this.links) {
6405
+ if (link.expiresAt <= now) {
6406
+ this.forget(key);
6407
+ }
6408
+ }
6409
+ }
6410
+ get size() {
6411
+ return this.links.size;
6412
+ }
6413
+ clear() {
6414
+ this.links.clear();
6415
+ this.keysByApprovalId.clear();
6416
+ }
6417
+ forget(key) {
6418
+ const link = this.links.get(key);
6419
+ if (!link) {
6420
+ return;
6421
+ }
6422
+ this.links.delete(key);
6423
+ if (link.approvalId !== null && this.keysByApprovalId.get(link.approvalId) === key) {
6424
+ this.keysByApprovalId.delete(link.approvalId);
6425
+ }
6426
+ }
6427
+ };
6428
+
5405
6429
  // src/bridge/idempotency-cache.ts
5406
6430
  import { createHash } from "node:crypto";
5407
6431
  var DEFAULT_TTL_MS = 10 * 60 * 1e3;
5408
- var DEFAULT_MAX_ENTRIES = 1e3;
6432
+ var DEFAULT_MAX_ENTRIES2 = 1e3;
5409
6433
  function stableStringify(value) {
5410
6434
  if (value === null || typeof value !== "object") {
5411
6435
  return JSON.stringify(value) ?? "null";
@@ -5428,7 +6452,7 @@ var IdempotencyCache = class {
5428
6452
  entries;
5429
6453
  constructor({
5430
6454
  ttlMs = DEFAULT_TTL_MS,
5431
- maxEntries = DEFAULT_MAX_ENTRIES,
6455
+ maxEntries = DEFAULT_MAX_ENTRIES2,
5432
6456
  now = () => Date.now()
5433
6457
  } = {}) {
5434
6458
  this.ttlMs = ttlMs;
@@ -5492,6 +6516,15 @@ var DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
5492
6516
  function inFlightIndexKey(worldId, idempotencyKey) {
5493
6517
  return JSON.stringify([worldId, idempotencyKey]);
5494
6518
  }
6519
+ var UNDISCOVERABLE_COMMANDS = new Set(
6520
+ COMMAND_NAMES.filter((name) => !DISCOVERABLE_COMMAND_NAMES.includes(name))
6521
+ );
6522
+ function withoutUndiscoverableCommands(session) {
6523
+ return {
6524
+ ...session,
6525
+ commands: session.commands.filter((name) => !UNDISCOVERABLE_COMMANDS.has(name))
6526
+ };
6527
+ }
5495
6528
  var BridgeSessionStore = class {
5496
6529
  activeBridgeSocket;
5497
6530
  activeSession;
@@ -5505,20 +6538,18 @@ var BridgeSessionStore = class {
5505
6538
  }
5506
6539
  registerBridge(socket, session) {
5507
6540
  const staleSocket = this.activeBridgeSocket && this.activeBridgeSocket !== socket ? this.activeBridgeSocket : null;
5508
- if (staleSocket) {
5509
- this.failPendingForBridge(staleSocket, "taken-over");
5510
- }
6541
+ const lostKeyedRequests = staleSocket ? this.failPendingForBridge(staleSocket, "taken-over") : [];
5511
6542
  this.activeBridgeSocket = socket;
5512
6543
  this.activeSession = session;
5513
6544
  if (staleSocket) {
5514
6545
  staleSocket.close(BRIDGE_TAKEOVER_CLOSE_CODE, BRIDGE_TAKEOVER_CLOSE_REASON);
5515
6546
  }
5516
- return { ok: true };
6547
+ return { ok: true, lostKeyedRequests };
5517
6548
  }
5518
6549
  getBridgeStatus() {
5519
6550
  return {
5520
6551
  connected: Boolean(this.activeBridgeSocket && this.activeSession),
5521
- session: this.activeSession
6552
+ session: this.activeSession ? withoutUndiscoverableCommands(this.activeSession) : null
5522
6553
  };
5523
6554
  }
5524
6555
  hasActiveBridge() {
@@ -5597,7 +6628,7 @@ var BridgeSessionStore = class {
5597
6628
  id: request.id,
5598
6629
  error: createProtocolError({
5599
6630
  code: ERROR_CODES.BRIDGE_TIMEOUT,
5600
- message: `Timed out waiting for Foundry bridge response to ${request.command} after ${requestTimeoutMs}ms; the request was forwarded so it MAY have committed \u2014 verify world state before retrying (a same idempotencyKey retry can fetch the cached late success once it lands)`,
6631
+ message: `Timed out waiting for Foundry bridge response to ${request.command} after ${requestTimeoutMs}ms; the request was forwarded so it MAY have committed \u2014 verify world state before retrying (a same idempotencyKey retry can fetch the cached late success once it lands, unless the bridge session carrying it ends first, after which that key is refused)`,
5601
6632
  details: {
5602
6633
  reason: "timeout",
5603
6634
  command: request.command,
@@ -5639,6 +6670,7 @@ var BridgeSessionStore = class {
5639
6670
  }
5640
6671
  }, requestTimeoutMs);
5641
6672
  this.pendingRequests.set(request.id, {
6673
+ command: request.command,
5642
6674
  waiters: [{ clientSocket, requestId: request.id }],
5643
6675
  timeout,
5644
6676
  bridgeSocket: this.activeBridgeSocket,
@@ -5654,7 +6686,7 @@ var BridgeSessionStore = class {
5654
6686
  resolveResponse(response) {
5655
6687
  const pendingRequest = this.pendingRequests.get(response.id);
5656
6688
  if (!pendingRequest) {
5657
- return { matched: false, idempotency: null, worldId: null };
6689
+ return { matched: false, command: null, idempotency: null, worldId: null };
5658
6690
  }
5659
6691
  clearTimeout(pendingRequest.timeout);
5660
6692
  this.forgetPending(response.id);
@@ -5663,17 +6695,18 @@ var BridgeSessionStore = class {
5663
6695
  }
5664
6696
  return {
5665
6697
  matched: true,
6698
+ command: pendingRequest.command,
5666
6699
  idempotency: pendingRequest.idempotency ?? null,
5667
6700
  worldId: pendingRequest.worldId ?? null
5668
6701
  };
5669
6702
  }
5670
6703
  removeSocket(socket) {
5671
6704
  if (this.activeBridgeSocket === socket) {
5672
- this.failPendingForBridge(socket, "disconnected");
6705
+ const lostKeyedRequests = this.failPendingForBridge(socket, "disconnected");
5673
6706
  this.activeBridgeSocket = null;
5674
6707
  this.activeSession = null;
5675
6708
  this.inFlightByKey.clear();
5676
- return;
6709
+ return lostKeyedRequests;
5677
6710
  }
5678
6711
  for (const [requestId, pendingRequest] of this.pendingRequests.entries()) {
5679
6712
  const remainingWaiters = pendingRequest.waiters.filter((waiter) => waiter.clientSocket !== socket);
@@ -5687,19 +6720,31 @@ var BridgeSessionStore = class {
5687
6720
  clearTimeout(pendingRequest.timeout);
5688
6721
  this.forgetPending(requestId);
5689
6722
  }
6723
+ return [];
5690
6724
  }
5691
6725
  failPendingForBridge(socket, reason) {
6726
+ const lostKeyedRequests = [];
5692
6727
  for (const [requestId, pendingRequest] of this.pendingRequests.entries()) {
5693
6728
  if (pendingRequest.bridgeSocket !== socket) {
5694
6729
  continue;
5695
6730
  }
5696
6731
  clearTimeout(pendingRequest.timeout);
5697
6732
  this.forgetPending(requestId);
6733
+ const keyed = pendingRequest.idempotency !== void 0 && pendingRequest.worldId !== void 0;
6734
+ if (pendingRequest.idempotency && pendingRequest.worldId !== void 0) {
6735
+ lostKeyedRequests.push({
6736
+ worldId: pendingRequest.worldId,
6737
+ key: pendingRequest.idempotency.key,
6738
+ fingerprint: pendingRequest.idempotency.fingerprint
6739
+ });
6740
+ }
6741
+ const cause = reason === "taken-over" ? "Authenticated Foundry bridge session was replaced by a newer socket from the same pairing while the request was pending" : "Authenticated Foundry bridge session disconnected while the request was pending";
6742
+ const retryAdvice = keyed ? "verify world state before retrying, then send the command again under a FRESH idempotencyKey (reusing the same key is refused for the dedupe window on this daemon)" : "verify world state before retrying";
5698
6743
  const disconnectError = createErrorResponse({
5699
6744
  id: requestId,
5700
6745
  error: createProtocolError({
5701
6746
  code: ERROR_CODES.BRIDGE_DISCONNECTED,
5702
- message: reason === "taken-over" ? "Authenticated Foundry bridge session was replaced by a newer socket from the same pairing while the request was pending; it was already forwarded so it MAY have committed \u2014 verify world state before retrying" : "Authenticated Foundry bridge session disconnected while the request was pending; it was already forwarded so it MAY have committed \u2014 verify world state before retrying (this is NOT idempotency-key safe: the disconnect path caches nothing, so a keyed retry re-forwards)",
6747
+ message: `${cause}; it was already forwarded so it MAY have committed \u2014 ${retryAdvice}`,
5703
6748
  details: { reason }
5704
6749
  })
5705
6750
  });
@@ -5707,6 +6752,7 @@ var BridgeSessionStore = class {
5707
6752
  sendJson(waiter.clientSocket, { ...disconnectError, id: waiter.requestId });
5708
6753
  }
5709
6754
  }
6755
+ return lostKeyedRequests;
5710
6756
  }
5711
6757
  clearAllPending() {
5712
6758
  for (const pendingRequest of this.pendingRequests.values()) {
@@ -5721,6 +6767,22 @@ var BridgeSessionStore = class {
5721
6767
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
5722
6768
  var DEFAULT_IDEMPOTENCY_TTL_MS = 10 * 60 * 1e3;
5723
6769
  var ACCEPTED_BRIDGE_RESPONSE_TYPES = Object.freeze([MESSAGE_TYPES.COMMAND_RESPONSE]);
6770
+ var CLI_HELLO_ORIGIN = {
6771
+ peer: PROTOCOL_COMPONENTS.CLI_DAEMON,
6772
+ handshake: PROTOCOL_HANDSHAKES.CLI_DAEMON
6773
+ };
6774
+ var MODULE_HELLO_ORIGIN = {
6775
+ peer: PROTOCOL_COMPONENTS.MODULE,
6776
+ handshake: PROTOCOL_HANDSHAKES.MODULE_DAEMON
6777
+ };
6778
+ var CLI_COMMAND_ORIGIN = {
6779
+ peer: PROTOCOL_COMPONENTS.CLI_DAEMON,
6780
+ handshake: PROTOCOL_HANDSHAKES.COMMAND_REQUEST
6781
+ };
6782
+ var CLI_CONTROL_ORIGIN = {
6783
+ peer: PROTOCOL_COMPONENTS.CLI_DAEMON,
6784
+ handshake: PROTOCOL_HANDSHAKES.DAEMON_REQUEST
6785
+ };
5724
6786
  function createBridgeHelloAck({
5725
6787
  ok,
5726
6788
  error,
@@ -5770,6 +6832,16 @@ function equalSecret(left, right) {
5770
6832
  function daemonEnvelope(type, id, operation, ok, result, error) {
5771
6833
  return { protocolVersion: PROTOCOL_VERSION, type, id, operation, ok, ...ok ? { result } : { error } };
5772
6834
  }
6835
+ function idempotencyConflict(requestId, command, idempotencyKey, remedy, clause = "was already used for") {
6836
+ return createErrorResponse({
6837
+ id: requestId,
6838
+ error: createProtocolError({
6839
+ code: ERROR_CODES.IDEMPOTENCY_KEY_CONFLICT,
6840
+ message: `Idempotency key "${idempotencyKey}" ${clause} a different ${command} request (same key, different command or params); use a fresh idempotencyKey for this new request, or ${remedy}`,
6841
+ details: { command, idempotencyKey }
6842
+ })
6843
+ });
6844
+ }
5773
6845
  function publicPendingPairing(entry) {
5774
6846
  return {
5775
6847
  code: entry.code,
@@ -5801,6 +6873,7 @@ var BridgeDaemon = class {
5801
6873
  wsMaxPayloadBytes;
5802
6874
  sessionStore;
5803
6875
  idempotencyCache;
6876
+ approvalLinks;
5804
6877
  cachedWorldId;
5805
6878
  server;
5806
6879
  connectionStates;
@@ -5814,6 +6887,7 @@ var BridgeDaemon = class {
5814
6887
  heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS,
5815
6888
  idempotencyTtlMs = DEFAULT_IDEMPOTENCY_TTL_MS,
5816
6889
  idempotencyCacheOptions = {},
6890
+ approvalLinkOptions = {},
5817
6891
  uploadLimitBytes = DEFAULT_UPLOAD_SIZE_LIMIT_BYTES,
5818
6892
  logger = pino({ level: process.env.FVTT_WORLD_CLI_LOG_LEVEL ?? "warn" })
5819
6893
  } = {}) {
@@ -5844,6 +6918,7 @@ var BridgeDaemon = class {
5844
6918
  ...idempotencyCacheOptions,
5845
6919
  ttlMs: idempotencyCacheOptions.ttlMs ?? idempotencyTtlMs
5846
6920
  });
6921
+ this.approvalLinks = new ApprovalIdempotencyLinks(approvalLinkOptions);
5847
6922
  this.cachedWorldId = null;
5848
6923
  this.server = null;
5849
6924
  this.connectionStates = /* @__PURE__ */ new WeakMap();
@@ -5902,7 +6977,7 @@ var BridgeDaemon = class {
5902
6977
  this.leaseExpiresAt = Date.now() + BRIDGE_LEASE_MS;
5903
6978
  this.activePairingId = null;
5904
6979
  }
5905
- this.sessionStore.removeSocket(socket);
6980
+ this.tombstoneLostKeyedRequests(this.sessionStore.removeSocket(socket));
5906
6981
  });
5907
6982
  });
5908
6983
  await new Promise((resolve7, reject) => {
@@ -5951,6 +7026,7 @@ var BridgeDaemon = class {
5951
7026
  this.heartbeatTimer = null;
5952
7027
  }
5953
7028
  this.idempotencyCache.clear();
7029
+ this.approvalLinks.clear();
5954
7030
  this.cachedWorldId = null;
5955
7031
  this.dropAwaitWaiters(() => true);
5956
7032
  this.sessionStore.clearAllPending();
@@ -5992,7 +7068,10 @@ var BridgeDaemon = class {
5992
7068
  const message = parsed.value;
5993
7069
  const validationResult = validateTransportMessage(message);
5994
7070
  if (!validationResult.ok) {
5995
- const protocolError = message.protocolVersion !== void 0 && message.protocolVersion !== PROTOCOL_VERSION ? getProtocolVersionError(String(message.protocolVersion)) : createProtocolError({
7071
+ const rejection = (origin) => message.protocolVersion !== void 0 && message.protocolVersion !== PROTOCOL_VERSION ? getProtocolVersionError(String(message.protocolVersion), {
7072
+ ...origin,
7073
+ reporter: PROTOCOL_COMPONENTS.CLI_DAEMON
7074
+ }) : createProtocolError({
5996
7075
  code: ERROR_CODES.INVALID_MESSAGE,
5997
7076
  message: "Invalid transport message",
5998
7077
  details: { errors: validationResult.errors }
@@ -6002,23 +7081,23 @@ var BridgeDaemon = class {
6002
7081
  protocolVersion: PROTOCOL_VERSION,
6003
7082
  type: MESSAGE_TYPES.CLIENT_HELLO_ACK,
6004
7083
  ok: false,
6005
- error: protocolError
7084
+ error: rejection(CLI_HELLO_ORIGIN)
6006
7085
  });
6007
7086
  } else if (state.role === "unknown" && message.type === MESSAGE_TYPES.PAIRING_REQUEST) {
6008
7087
  sendJson(socket, {
6009
7088
  protocolVersion: PROTOCOL_VERSION,
6010
7089
  type: MESSAGE_TYPES.PAIRING_RESULT,
6011
7090
  ok: false,
6012
- error: protocolError
7091
+ error: rejection(MODULE_HELLO_ORIGIN)
6013
7092
  });
6014
7093
  } else if (state.role === "unknown" && message.type === MESSAGE_TYPES.BRIDGE_HELLO) {
6015
- sendJson(socket, createBridgeHelloAck({ ok: false, error: protocolError }));
7094
+ sendJson(socket, createBridgeHelloAck({ ok: false, error: rejection(MODULE_HELLO_ORIGIN) }));
6016
7095
  } else if (state.role === "cli" && message.type === MESSAGE_TYPES.COMMAND_REQUEST) {
6017
7096
  sendJson(
6018
7097
  socket,
6019
7098
  createErrorResponse({
6020
7099
  id: typeof message.id === "string" ? message.id : `invalid_${randomUUID2()}`,
6021
- error: protocolError
7100
+ error: rejection(CLI_COMMAND_ORIGIN)
6022
7101
  })
6023
7102
  );
6024
7103
  return;
@@ -6027,7 +7106,14 @@ var BridgeDaemon = class {
6027
7106
  const operation = typeof message.operation === "string" && message.operation ? message.operation : "unknown";
6028
7107
  sendJson(
6029
7108
  socket,
6030
- daemonEnvelope(MESSAGE_TYPES.DAEMON_RESPONSE, id, operation, false, void 0, protocolError)
7109
+ daemonEnvelope(
7110
+ MESSAGE_TYPES.DAEMON_RESPONSE,
7111
+ id,
7112
+ operation,
7113
+ false,
7114
+ void 0,
7115
+ rejection(CLI_CONTROL_ORIGIN)
7116
+ )
6031
7117
  );
6032
7118
  return;
6033
7119
  }
@@ -6143,7 +7229,7 @@ var BridgeDaemon = class {
6143
7229
  state.role = "unknown";
6144
7230
  state.pairingId = null;
6145
7231
  }
6146
- this.sessionStore.removeSocket(socket);
7232
+ this.tombstoneLostKeyedRequests(this.sessionStore.removeSocket(socket));
6147
7233
  socket.close(closeCode, closeReason);
6148
7234
  }
6149
7235
  stampPairingLastSeen(pairingId) {
@@ -6407,9 +7493,11 @@ var BridgeDaemon = class {
6407
7493
  const idempotencyScope = `${pairing.pairingId}:${pairing.origin}:${newSession.world.id}`;
6408
7494
  if (this.cachedWorldId !== null && this.cachedWorldId !== idempotencyScope) {
6409
7495
  this.idempotencyCache.clear();
7496
+ this.approvalLinks.clear();
6410
7497
  }
6411
7498
  this.cachedWorldId = idempotencyScope;
6412
- this.sessionStore.registerBridge(socket, newSession);
7499
+ const { lostKeyedRequests } = this.sessionStore.registerBridge(socket, newSession);
7500
+ this.tombstoneLostKeyedRequests(lostKeyedRequests);
6413
7501
  sendJson(
6414
7502
  socket,
6415
7503
  createBridgeHelloAck({
@@ -6423,9 +7511,51 @@ var BridgeDaemon = class {
6423
7511
  this.logger.warn({ message }, "Ignoring non-response message from bridge socket");
6424
7512
  return;
6425
7513
  }
6426
- const { idempotency, worldId: matchedWorldId } = this.sessionStore.resolveResponse(message);
6427
- if (idempotency && message.ok === true && (socket === this.sessionStore.activeBridgeSocket || matchedWorldId !== null && matchedWorldId === this.cachedWorldId)) {
7514
+ const { command, idempotency, worldId: matchedWorldId } = this.sessionStore.resolveResponse(message);
7515
+ const inScope = socket === this.sessionStore.activeBridgeSocket || matchedWorldId !== null && matchedWorldId === this.cachedWorldId;
7516
+ if (!inScope) {
7517
+ return;
7518
+ }
7519
+ if (idempotency && message.ok === true) {
6428
7520
  this.idempotencyCache.storeIfAbsent(idempotency.key, idempotency.fingerprint, message);
7521
+ this.approvalLinks.release(idempotency.key);
7522
+ return;
7523
+ }
7524
+ if (idempotency) {
7525
+ const pendingApproval = readPendingApprovalDetails(message);
7526
+ if (pendingApproval) {
7527
+ this.approvalLinks.record({
7528
+ key: idempotency.key,
7529
+ fingerprint: idempotency.fingerprint,
7530
+ pendingResponse: message,
7531
+ ...pendingApproval
7532
+ });
7533
+ return;
7534
+ }
7535
+ this.approvalLinks.release(idempotency.key);
7536
+ return;
7537
+ }
7538
+ const settlement = readApprovalSettlement(command, message);
7539
+ const promoted = this.approvalLinks.settle(settlement);
7540
+ if (promoted && settlement.kind === "promote") {
7541
+ this.idempotencyCache.storeIfAbsent(promoted.key, promoted.fingerprint, settlement.response);
7542
+ }
7543
+ }
7544
+ tombstoneLostKeyedRequests(lostKeyedRequests) {
7545
+ for (const lost of lostKeyedRequests) {
7546
+ if (lost.worldId !== this.cachedWorldId) {
7547
+ this.approvalLinks.release(lost.key);
7548
+ continue;
7549
+ }
7550
+ if (this.idempotencyCache.lookup(lost.key, lost.fingerprint).status !== "miss") {
7551
+ this.approvalLinks.release(lost.key);
7552
+ continue;
7553
+ }
7554
+ this.approvalLinks.recordLostInFlight({
7555
+ key: lost.key,
7556
+ fingerprint: lost.fingerprint,
7557
+ retainMs: this.idempotencyCache.ttlMs
7558
+ });
6429
7559
  }
6430
7560
  }
6431
7561
  handleCliMessage(socket, state, message) {
@@ -6459,14 +7589,40 @@ var BridgeDaemon = class {
6459
7589
  return;
6460
7590
  }
6461
7591
  if (lookup.status === "conflict") {
7592
+ sendJson(
7593
+ socket,
7594
+ idempotencyConflict(
7595
+ requestId,
7596
+ String(message.command),
7597
+ idempotencyKey,
7598
+ "resend the byte-identical original request to fetch its cached result"
7599
+ )
7600
+ );
7601
+ return;
7602
+ }
7603
+ const link = this.approvalLinks.lookup(idempotencyKey, fingerprint);
7604
+ if (link.status === "conflict") {
7605
+ sendJson(
7606
+ socket,
7607
+ idempotencyConflict(
7608
+ requestId,
7609
+ String(message.command),
7610
+ idempotencyKey,
7611
+ link.approvalId === null ? "verify world state and use a fresh idempotencyKey" : "resend the byte-identical original request to keep waiting on the approval it already created"
7612
+ )
7613
+ );
7614
+ return;
7615
+ }
7616
+ if (link.status === "lost-in-flight") {
6462
7617
  sendJson(
6463
7618
  socket,
6464
7619
  createErrorResponse({
6465
7620
  id: requestId,
6466
7621
  error: createProtocolError({
6467
- code: ERROR_CODES.IDEMPOTENCY_KEY_CONFLICT,
6468
- message: `Idempotency key "${idempotencyKey}" was already used for a different ${message.command} request (same key, different command or params); use a fresh idempotencyKey for this new request, or resend the byte-identical original request to fetch its cached result`,
7622
+ code: ERROR_CODES.BRIDGE_DISCONNECTED,
7623
+ message: `Idempotency key "${idempotencyKey}" belongs to a ${message.command} request that was already forwarded to Foundry when the bridge session ended, so this daemon never learned whether it ran: it may have changed the world, and it may already be waiting on a GM approval that is still actionable. Read the documents that request would have written before anything else, report what you found, and send the command again only under a fresh idempotencyKey.`,
6469
7624
  details: {
7625
+ reason: "lost-in-flight",
6470
7626
  command: message.command,
6471
7627
  idempotencyKey
6472
7628
  }
@@ -6475,6 +7631,28 @@ var BridgeDaemon = class {
6475
7631
  );
6476
7632
  return;
6477
7633
  }
7634
+ if (link.status === "pending") {
7635
+ sendJson(socket, { ...link.response, id: requestId });
7636
+ return;
7637
+ }
7638
+ if (link.status === "indeterminate") {
7639
+ sendJson(
7640
+ socket,
7641
+ createErrorResponse({
7642
+ id: requestId,
7643
+ error: createProtocolError({
7644
+ code: ERROR_CODES.APPROVAL_UNKNOWN,
7645
+ message: `Idempotency key "${idempotencyKey}" belongs to a ${message.command} request whose GM approval ended without a readable outcome, so this daemon cannot say whether it ran: it may never have started, or it may have completed and changed the world. Read the documents that request would have written before anything else, report what you found, and send the command again only under a fresh idempotencyKey.`,
7646
+ details: {
7647
+ command: message.command,
7648
+ idempotencyKey,
7649
+ approvalId: link.approvalId
7650
+ }
7651
+ })
7652
+ })
7653
+ );
7654
+ return;
7655
+ }
6478
7656
  if (this.cachedWorldId !== null) {
6479
7657
  const inFlight = this.sessionStore.findInFlight(this.cachedWorldId, idempotencyKey);
6480
7658
  if (inFlight) {
@@ -6489,22 +7667,52 @@ var BridgeDaemon = class {
6489
7667
  } else {
6490
7668
  sendJson(
6491
7669
  socket,
6492
- createErrorResponse({
6493
- id: requestId,
6494
- error: createProtocolError({
6495
- code: ERROR_CODES.IDEMPOTENCY_KEY_CONFLICT,
6496
- message: `Idempotency key "${idempotencyKey}" is already in flight for a different ${message.command} request (same key, different command or params); use a fresh idempotencyKey for this new request, or resend the byte-identical original request to coalesce onto the in-flight one`,
6497
- details: {
6498
- command: message.command,
6499
- idempotencyKey
6500
- }
6501
- })
6502
- })
7670
+ idempotencyConflict(
7671
+ requestId,
7672
+ String(message.command),
7673
+ idempotencyKey,
7674
+ "resend the byte-identical original request to coalesce onto the in-flight one",
7675
+ "is already in flight for"
7676
+ )
6503
7677
  );
6504
7678
  return;
6505
7679
  }
6506
7680
  }
6507
7681
  }
7682
+ if (this.cachedWorldId !== null) {
7683
+ const reserved = this.approvalLinks.reserve({
7684
+ key: idempotencyKey,
7685
+ fingerprint,
7686
+ retainMs: this.requestTimeoutMs + this.idempotencyCache.ttlMs
7687
+ });
7688
+ if (!reserved.ok && reserved.reason === "conflict") {
7689
+ sendJson(
7690
+ socket,
7691
+ idempotencyConflict(
7692
+ requestId,
7693
+ String(message.command),
7694
+ idempotencyKey,
7695
+ "resend the byte-identical original request to reuse the reservation it already holds",
7696
+ "is still reserved for"
7697
+ )
7698
+ );
7699
+ return;
7700
+ }
7701
+ if (!reserved.ok) {
7702
+ sendJson(
7703
+ socket,
7704
+ createErrorResponse({
7705
+ id: requestId,
7706
+ error: createProtocolError({
7707
+ code: ERROR_CODES.IDEMPOTENCY_STORE_FULL,
7708
+ message: `This daemon is holding its maximum of ${this.approvalLinks.maxEntries} indeterminate idempotency keys, so it cannot guarantee that a ${message.command} request forwarded under key "${idempotencyKey}" would stay deduplicated if the bridge session ended mid-flight; nothing was forwarded and the world is unchanged. Retry once earlier keys settle or their dedupe window expires.`,
7709
+ details: { command: message.command, idempotencyKey }
7710
+ })
7711
+ })
7712
+ );
7713
+ return;
7714
+ }
7715
+ }
6508
7716
  idempotency = { key: idempotencyKey, fingerprint };
6509
7717
  }
6510
7718
  const result = this.sessionStore.forwardRequest({
@@ -6516,6 +7724,9 @@ var BridgeDaemon = class {
6516
7724
  ...idempotency && this.cachedWorldId !== null ? { worldId: this.cachedWorldId } : {}
6517
7725
  });
6518
7726
  if (!result.ok) {
7727
+ if (idempotency) {
7728
+ this.approvalLinks.release(idempotency.key);
7729
+ }
6519
7730
  sendJson(
6520
7731
  socket,
6521
7732
  createErrorResponse({
@@ -6585,7 +7796,8 @@ async function sendCommand({
6585
7796
  command,
6586
7797
  params = {},
6587
7798
  timeoutMs = DEFAULT_CLIENT_TIMEOUT_MS,
6588
- maxPayloadBytes = DEFAULT_WS_MAX_PAYLOAD_BYTES
7799
+ maxPayloadBytes = DEFAULT_WS_MAX_PAYLOAD_BYTES,
7800
+ signal
6589
7801
  }) {
6590
7802
  warnIfClientTimeoutUnsafe(timeoutMs);
6591
7803
  const built = buildValidatedRequest(command, params);
@@ -6613,11 +7825,32 @@ async function sendCommand({
6613
7825
  }, timeoutMs);
6614
7826
  function cleanup() {
6615
7827
  clearTimeout(timeout);
7828
+ signal?.removeEventListener("abort", onAbort);
6616
7829
  socket.removeAllListeners();
6617
7830
  if (socket.readyState === socket.OPEN || socket.readyState === socket.CONNECTING) {
6618
7831
  socket.close();
6619
7832
  }
6620
7833
  }
7834
+ function onAbort() {
7835
+ if (settled) {
7836
+ return;
7837
+ }
7838
+ settled = true;
7839
+ cleanup();
7840
+ socket.terminate();
7841
+ reject(
7842
+ new DaemonTransportError(
7843
+ ERROR_CODES.DAEMON_UNAVAILABLE,
7844
+ `Stopped waiting for the daemon response to ${command}`,
7845
+ { reason: "closed", command }
7846
+ )
7847
+ );
7848
+ }
7849
+ if (signal?.aborted) {
7850
+ onAbort();
7851
+ return;
7852
+ }
7853
+ signal?.addEventListener("abort", onAbort);
6621
7854
  socket.once("open", () => {
6622
7855
  opened = true;
6623
7856
  socket.send(JSON.stringify(createClientHello({ credential: deviceCredential ?? "" })));
@@ -6739,6 +7972,13 @@ async function connectDaemonClient({
6739
7972
  });
6740
7973
  socket.once("message", (data) => {
6741
7974
  const parsed = parseBridgeMessage(normalizeIncomingData(data));
7975
+ const skew = parsed.ok ? protocolVersionSkewError(parsed.value) : null;
7976
+ if (skew) {
7977
+ clearTimeout(timer);
7978
+ reject(skew);
7979
+ socket.close();
7980
+ return;
7981
+ }
6742
7982
  const validation = parsed.ok ? validateTransportMessage(parsed.value) : { ok: false };
6743
7983
  const message = parsed.ok && validation.ok ? parsed.value : null;
6744
7984
  if (!message || message.type !== MESSAGE_TYPES.CLIENT_HELLO_ACK || !message.ok) {
@@ -6820,7 +8060,7 @@ async function connectDaemonClient({
6820
8060
  );
6821
8061
  });
6822
8062
  return {
6823
- async send({ command, params = {}, timeoutMs: timeoutMs2 = DEFAULT_CLIENT_TIMEOUT_MS }) {
8063
+ async send({ command, params = {}, timeoutMs: timeoutMs2 = DEFAULT_CLIENT_TIMEOUT_MS, signal }) {
6824
8064
  warnIfClientTimeoutUnsafe(timeoutMs2);
6825
8065
  if (closedError) {
6826
8066
  throw closedError;
@@ -6831,8 +8071,21 @@ async function connectDaemonClient({
6831
8071
  }
6832
8072
  const request = built.request;
6833
8073
  return await new Promise((resolve7, reject) => {
8074
+ function onAbort() {
8075
+ clearTimeout(timer);
8076
+ pending.delete(request.id);
8077
+ signal?.removeEventListener("abort", onAbort);
8078
+ reject(
8079
+ new DaemonTransportError(
8080
+ ERROR_CODES.DAEMON_UNAVAILABLE,
8081
+ `Stopped waiting for the daemon response to ${command}`,
8082
+ { reason: "closed", command }
8083
+ )
8084
+ );
8085
+ }
6834
8086
  const timer = setTimeout(() => {
6835
8087
  pending.delete(request.id);
8088
+ signal?.removeEventListener("abort", onAbort);
6836
8089
  reject(
6837
8090
  new DaemonTransportError(
6838
8091
  ERROR_CODES.DAEMON_UNAVAILABLE,
@@ -6841,7 +8094,22 @@ async function connectDaemonClient({
6841
8094
  )
6842
8095
  );
6843
8096
  }, timeoutMs2);
6844
- pending.set(request.id, { resolve: resolve7, reject, timer });
8097
+ if (signal?.aborted) {
8098
+ onAbort();
8099
+ return;
8100
+ }
8101
+ signal?.addEventListener("abort", onAbort);
8102
+ pending.set(request.id, {
8103
+ resolve: (envelope) => {
8104
+ signal?.removeEventListener("abort", onAbort);
8105
+ resolve7(envelope);
8106
+ },
8107
+ reject: (error) => {
8108
+ signal?.removeEventListener("abort", onAbort);
8109
+ reject(error);
8110
+ },
8111
+ timer
8112
+ });
6845
8113
  socket.send(JSON.stringify(request));
6846
8114
  });
6847
8115
  },
@@ -7041,6 +8309,38 @@ function toTransportErrorEnvelope(error) {
7041
8309
  error instanceof Error ? error.message : String(error)
7042
8310
  );
7043
8311
  }
8312
+ var TRANSPORT_DETAIL_REASONS = /* @__PURE__ */ new Set([
8313
+ "timeout",
8314
+ "disconnected",
8315
+ "invalid_json",
8316
+ "unexpected_type",
8317
+ "closed",
8318
+ "connect_error"
8319
+ ]);
8320
+ function normalizeTransportMessage(message) {
8321
+ if (/ECONNREFUSED/.test(message)) {
8322
+ return "Could not connect to the daemon (connection refused). Is `fvtt-world-cli bridge serve` running?";
8323
+ }
8324
+ if (/ETIMEDOUT/.test(message)) {
8325
+ return "Could not connect to the daemon (connection timed out).";
8326
+ }
8327
+ if (/ENOTFOUND|EAI_AGAIN/.test(message)) {
8328
+ return "Could not resolve the daemon host.";
8329
+ }
8330
+ if (/ECONNRESET/.test(message)) {
8331
+ return "The daemon connection was reset.";
8332
+ }
8333
+ return message;
8334
+ }
8335
+ function renderProtocolError(error) {
8336
+ const code = error?.code ?? "UNKNOWN_ERROR";
8337
+ const message = normalizeTransportMessage(String(error?.message ?? "Unknown error"));
8338
+ const reason = error?.details?.reason;
8339
+ const showDetails = Boolean(error?.details) && !(typeof reason === "string" && TRANSPORT_DETAIL_REASONS.has(reason));
8340
+ const details = showDetails ? `
8341
+ ${JSON.stringify(error?.details, null, 2)}` : "";
8342
+ return `${code}: ${message}${details}`;
8343
+ }
7044
8344
  function planLocalError(error) {
7045
8345
  if (error instanceof ExecConnectError) {
7046
8346
  return {
@@ -7125,7 +8425,7 @@ function planCliErrorOutput(error, jsonMode, options = {}) {
7125
8425
  }
7126
8426
 
7127
8427
  // src/program.ts
7128
- import { Command as Command10, Option as Option13 } from "commander";
8428
+ import { Command as Command10, Option as Option16 } from "commander";
7129
8429
 
7130
8430
  // src/commands/actor.ts
7131
8431
  import { Option as Option3 } from "commander";
@@ -7267,6 +8567,49 @@ function parseOwnershipUsers(value) {
7267
8567
  }
7268
8568
  return object;
7269
8569
  }
8570
+ function parseJsonValue(value, label) {
8571
+ try {
8572
+ return JSON.parse(value);
8573
+ } catch (error) {
8574
+ throw new InvalidArgumentError(
8575
+ error instanceof Error ? `${label} must be a JSON literal \u2014 true, 42, "text", [..] or {..}, so a quoted string stays distinct from a boolean or a number: ${error.message}` : `${label} must be a JSON literal \u2014 true, 42, "text", [..] or {..}`
8576
+ );
8577
+ }
8578
+ }
8579
+ var USER_ROLES = [1, 2, 3, 4];
8580
+ function parseUserRole(value) {
8581
+ const trimmed = value.trim();
8582
+ const parsed = INTEGER_PATTERN.test(trimmed) ? Number(trimmed) : NaN;
8583
+ if (!Number.isInteger(parsed) || !USER_ROLES.includes(parsed)) {
8584
+ throw new InvalidArgumentError(
8585
+ `Expected a user role (1=player, 2=trusted player, 3=assistant GM, 4=gamemaster), received ${value}`
8586
+ );
8587
+ }
8588
+ return parsed;
8589
+ }
8590
+ function parseUserPermissions(value) {
8591
+ const object = parseJsonObject(value, "--permissions-json");
8592
+ const names = Object.keys(object);
8593
+ if (names.length === 0) {
8594
+ throw new InvalidArgumentError("--permissions-json must name at least one permission");
8595
+ }
8596
+ for (const name of names) {
8597
+ const entry = object[name];
8598
+ if (typeof entry !== "boolean" && entry !== null) {
8599
+ throw new InvalidArgumentError(
8600
+ `--permissions-json values must be true (grant), false (revoke) or null (drop the override so the role default applies); ${name} = ${JSON.stringify(entry)}`
8601
+ );
8602
+ }
8603
+ }
8604
+ return object;
8605
+ }
8606
+ function parseSettingItems(value, label) {
8607
+ const items = parseJsonObjectArray(value, label);
8608
+ if (items.length === 0) {
8609
+ throw new InvalidArgumentError(`${label} must contain at least one setting item`);
8610
+ }
8611
+ return items;
8612
+ }
7270
8613
  function parseIdList(value) {
7271
8614
  const ids = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
7272
8615
  if (ids.length === 0) {
@@ -8438,6 +9781,120 @@ function createSceneRegionBehaviorCloneParams(options) {
8438
9781
  })
8439
9782
  };
8440
9783
  }
9784
+ var EXECUTABLE_BEHAVIOR_TYPE = "executeMacro";
9785
+ function nullableField(key, value, cleared) {
9786
+ return cleared ? { [key]: null } : value !== void 0 ? { [key]: value } : {};
9787
+ }
9788
+ function buildUserFields(options) {
9789
+ return {
9790
+ ...stringField("color", options.color),
9791
+ ...stringField("pronouns", options.pronouns),
9792
+ ...nullableField("avatar", options.avatar, options.clearAvatar),
9793
+ ...nullableField("character", options.character, options.clearCharacter),
9794
+ ...optionalJsonObject(options.dataJson, "--data-json") ?? {},
9795
+ ...optionalJsonObject(options.patchJson, "--patch-json") ?? {}
9796
+ };
9797
+ }
9798
+ function createSettingSetParams(options) {
9799
+ return { namespace: options.namespace, key: options.key, value: options.valueJson };
9800
+ }
9801
+ async function resolveSettingItems(options, dependencies) {
9802
+ if (options.itemsStdin) {
9803
+ if (dependencies.stdin.isTTY) {
9804
+ throw new CommanderError2(
9805
+ 1,
9806
+ "fvtt-world-cli.settingItemsNoStdin",
9807
+ "--items-stdin requires the setting items piped on stdin as a JSON array; refusing to read from an interactive terminal."
9808
+ );
9809
+ }
9810
+ return parseSettingItems(await text(dependencies.stdin), "--items-stdin");
9811
+ }
9812
+ if (options.itemsJson === void 0) {
9813
+ throw new CommanderError2(
9814
+ 1,
9815
+ "fvtt-world-cli.settingItemsRequired",
9816
+ "setting set-many needs the items: pass --items-json with a JSON array of {namespace,key,value} objects, or --items-stdin to read that array from stdin."
9817
+ );
9818
+ }
9819
+ return options.itemsJson;
9820
+ }
9821
+ function buildExecutableBehaviorFields(options) {
9822
+ const system = {
9823
+ ...stringField("uuid", options.macroUuid),
9824
+ ...options.events ? { events: options.events } : {},
9825
+ ...booleanField("everyone", options.everyone),
9826
+ ...options.systemJson ? parseJsonObject(options.systemJson, "--system-json") : {}
9827
+ };
9828
+ return {
9829
+ ...stringField("name", options.name),
9830
+ ...booleanField("disabled", options.disabled),
9831
+ ...Object.keys(system).length > 0 ? { system } : {}
9832
+ };
9833
+ }
9834
+ function createExecutableBehaviorCreateParams(options) {
9835
+ return {
9836
+ sceneId: options.sceneId,
9837
+ regionId: options.regionId,
9838
+ data: {
9839
+ type: EXECUTABLE_BEHAVIOR_TYPE,
9840
+ ...buildExecutableBehaviorFields(options),
9841
+ ...optionalJsonObject(options.dataJson, "--data-json") ?? {}
9842
+ }
9843
+ };
9844
+ }
9845
+ function createExecutableBehaviorUpdateParams(options) {
9846
+ return {
9847
+ sceneId: options.sceneId,
9848
+ regionId: options.regionId,
9849
+ behaviorId: options.behaviorId,
9850
+ patch: assertNonEmptyPatch({
9851
+ ...buildExecutableBehaviorFields(options),
9852
+ ...optionalJsonObject(options.patchJson, "--patch-json") ?? {}
9853
+ })
9854
+ };
9855
+ }
9856
+ function createExecutableBehaviorCloneParams(options) {
9857
+ return {
9858
+ sceneId: options.sceneId,
9859
+ regionId: options.regionId,
9860
+ behaviorId: options.behaviorId,
9861
+ ...optionalPatch({
9862
+ ...buildExecutableBehaviorFields(options),
9863
+ ...optionalJsonObject(options.patchJson, "--patch-json") ?? {}
9864
+ })
9865
+ };
9866
+ }
9867
+ function createMacroExecuteParams(options) {
9868
+ const scope = {
9869
+ ...stringField("actorId", options.actorId),
9870
+ ...stringField("sceneId", options.sceneId),
9871
+ ...stringField("tokenId", options.tokenId),
9872
+ ...jsonObjectField("args", options.argsJson, "--args-json")
9873
+ };
9874
+ return {
9875
+ macroId: options.macroId,
9876
+ ...Object.keys(scope).length > 0 ? { scope } : {},
9877
+ ...numberField("timeoutMs", options.macroTimeoutMs)
9878
+ };
9879
+ }
9880
+ function createUserCreateParams(options) {
9881
+ return {
9882
+ data: {
9883
+ name: options.name,
9884
+ ...numberField("role", options.role),
9885
+ ...buildUserFields(options)
9886
+ }
9887
+ };
9888
+ }
9889
+ function createUserUpdateParams(options) {
9890
+ return {
9891
+ userId: options.userId,
9892
+ patch: assertNonEmptyPatch({
9893
+ ...stringField("name", options.name),
9894
+ ...buildUserFields(options)
9895
+ })
9896
+ };
9897
+ }
8441
9898
  function createFileUploadParams(options, uploadLimitBytes) {
8442
9899
  return {
8443
9900
  path: options.path,
@@ -8450,6 +9907,248 @@ function createFileUploadParams(options, uploadLimitBytes) {
8450
9907
  import { createInterface } from "node:readline";
8451
9908
  import { CommanderError as CommanderError3 } from "commander";
8452
9909
 
9910
+ // src/park-polling.ts
9911
+ var AWAIT_FLOOR_MARGIN_MS = 5e3;
9912
+ var AWAIT_EMPTY_POLL_DELAY_MS = 250;
9913
+
9914
+ // src/approval-wait.ts
9915
+ var APPROVAL_AWAIT_CLIENT_TIMEOUT_FLOOR_MS = APPROVAL_AWAIT_PARK_CAP_MS + AWAIT_FLOOR_MARGIN_MS;
9916
+ var APPROVAL_EMPTY_POLL_DELAY_MS = AWAIT_EMPTY_POLL_DELAY_MS;
9917
+ var APPROVAL_RETRY_BASE_DELAY_MS = 500;
9918
+ var APPROVAL_RETRY_MAX_DELAY_MS = 5e3;
9919
+ var APPROVAL_RETRY_MARGIN_MS = 3e4;
9920
+ var TRANSIENT_TRANSPORT_CODES = /* @__PURE__ */ new Set([
9921
+ ERROR_CODES.DAEMON_UNAVAILABLE,
9922
+ ERROR_CODES.BRIDGE_NOT_READY,
9923
+ ERROR_CODES.BRIDGE_DISCONNECTED,
9924
+ ERROR_CODES.BRIDGE_TIMEOUT
9925
+ ]);
9926
+ function readPendingApproval(response) {
9927
+ const details = readPendingApprovalDetails(response);
9928
+ if (!details) {
9929
+ return null;
9930
+ }
9931
+ const command = response.error?.details?.command;
9932
+ return { ...details, command: typeof command === "string" ? command : "unknown" };
9933
+ }
9934
+ function describeTransportFailure(envelope) {
9935
+ return `${envelope.error?.code ?? ERROR_CODES.INTERNAL_ERROR}: ${envelope.error?.message ?? "unknown error"}`;
9936
+ }
9937
+ async function awaitApprovalOutcome({
9938
+ pendingResponse,
9939
+ send,
9940
+ stderr,
9941
+ timeoutMs,
9942
+ reconnect,
9943
+ onCancelRequested,
9944
+ signalScope = process,
9945
+ now = () => Date.now(),
9946
+ sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms))
9947
+ }) {
9948
+ const pending = readPendingApproval(pendingResponse);
9949
+ if (!pending) {
9950
+ return pendingResponse;
9951
+ }
9952
+ const { approvalId, expiresAt, command } = pending;
9953
+ const responseId = pendingResponse.id;
9954
+ const pollTimeoutMs = Math.max(timeoutMs ?? 0, APPROVAL_AWAIT_CLIENT_TIMEOUT_FLOOR_MS);
9955
+ const cancelTimeoutMs = timeoutMs ?? DEFAULT_CLIENT_TIMEOUT_MS;
9956
+ function terminal(code, message, details) {
9957
+ return { ...localErrorEnvelope(code, message, details), id: responseId };
9958
+ }
9959
+ function indeterminate(message, details) {
9960
+ return terminal(
9961
+ ERROR_CODES.APPROVAL_UNKNOWN,
9962
+ `${message} The outcome is indeterminate: command ${command} may never have started, may be running now, or may have completed and changed the world. Read the documents it would have written before anything else, report what you found, and if you decide to send it again, send it under a fresh idempotency key.`,
9963
+ { approvalId, command, ...details }
9964
+ );
9965
+ }
9966
+ function outcomeEnvelope(outcome) {
9967
+ if (outcome === "denied") {
9968
+ return terminal(
9969
+ ERROR_CODES.APPROVAL_DENIED,
9970
+ `The GM of this bridge denied command ${command}. It never ran and no world state changed: a denial is decided before execution starts, so nothing was partially applied. The verdict is terminal for this invocation and is not a transient failure to retry in a loop. Report the denial to the user; sending the same command again only makes sense once that human decision changes.`,
9971
+ { approvalId, command }
9972
+ );
9973
+ }
9974
+ if (outcome === "timeout") {
9975
+ return terminal(
9976
+ ERROR_CODES.APPROVAL_TIMEOUT,
9977
+ `The approval for command ${command} expired before a GM decided it. It never ran and no world state changed: the expiry is decided before execution starts. The verdict is terminal for this invocation. Before sending the command again, check that a GM is present at the Foundry client holding this bridge and able to answer its approval window, and tell the user the decision is waiting on them.`,
9978
+ { approvalId, command }
9979
+ );
9980
+ }
9981
+ if (outcome === "cancelled") {
9982
+ return terminal(
9983
+ ERROR_CODES.APPROVAL_CANCELLED,
9984
+ `The approval for command ${command} was cancelled before the GM decided it. The cancellation won the decision, so the command never ran and no world state changed. The verdict is terminal for this invocation, and sending the command again is safe: it asks the GM for a new approval.`,
9985
+ { approvalId, command }
9986
+ );
9987
+ }
9988
+ return indeterminate(
9989
+ `The GM client reported an approval outcome this CLI does not understand (${outcome}).`,
9990
+ { outcome }
9991
+ );
9992
+ }
9993
+ function readAwaitResult(result) {
9994
+ const report = result ?? {};
9995
+ if (report.status === "pending") {
9996
+ return { kind: "pending" };
9997
+ }
9998
+ if (report.status !== "resolved") {
9999
+ return {
10000
+ kind: "settled",
10001
+ envelope: indeterminate("The GM client reported an approval status this CLI does not understand.", {
10002
+ status: typeof report.status === "string" ? report.status : null
10003
+ })
10004
+ };
10005
+ }
10006
+ if (report.outcome !== "approved") {
10007
+ return { kind: "settled", envelope: outcomeEnvelope(String(report.outcome)) };
10008
+ }
10009
+ if (!isCommandResponseEnvelope(report.response, [MESSAGE_TYPES.COMMAND_RESPONSE])) {
10010
+ return {
10011
+ kind: "settled",
10012
+ envelope: indeterminate("The GM allowed the command, but its response was not delivered.", {
10013
+ outcome: "approved"
10014
+ })
10015
+ };
10016
+ }
10017
+ return { kind: "settled", envelope: report.response };
10018
+ }
10019
+ async function pollOnce(signal, waitMs = APPROVAL_AWAIT_PARK_CAP_MS) {
10020
+ let response;
10021
+ try {
10022
+ response = await send({
10023
+ command: APPROVAL_AWAIT_COMMAND,
10024
+ params: { approvalId, waitMs },
10025
+ timeoutMs: pollTimeoutMs,
10026
+ ...signal ? { signal } : {}
10027
+ });
10028
+ } catch (error) {
10029
+ return { kind: "transport", envelope: toTransportErrorEnvelope(error), connectionLost: true };
10030
+ }
10031
+ if (response.ok) {
10032
+ return readAwaitResult(response.result);
10033
+ }
10034
+ if (TRANSIENT_TRANSPORT_CODES.has(response.error?.code ?? "")) {
10035
+ return { kind: "transport", envelope: response, connectionLost: false };
10036
+ }
10037
+ return { kind: "settled", envelope: response };
10038
+ }
10039
+ async function requestCancellation() {
10040
+ let response;
10041
+ try {
10042
+ response = await send({
10043
+ command: APPROVAL_CANCEL_COMMAND,
10044
+ params: { approvalId },
10045
+ timeoutMs: cancelTimeoutMs
10046
+ });
10047
+ } catch (error) {
10048
+ return {
10049
+ kind: "settled",
10050
+ envelope: indeterminate(
10051
+ `Cancellation of command ${command} could not be confirmed: the GM client could not be reached (${describeTransportFailure(toTransportErrorEnvelope(error))}).`,
10052
+ { cancellation: "unconfirmed" }
10053
+ )
10054
+ };
10055
+ }
10056
+ if (!response.ok) {
10057
+ return {
10058
+ kind: "settled",
10059
+ envelope: indeterminate(
10060
+ `Cancellation of command ${command} could not be confirmed: the cancellation request itself failed (${describeTransportFailure(response)}).`,
10061
+ { cancellation: "unconfirmed" }
10062
+ )
10063
+ };
10064
+ }
10065
+ const status = response.result?.status;
10066
+ if (status === "cancelled") {
10067
+ return { kind: "settled", envelope: outcomeEnvelope("cancelled") };
10068
+ }
10069
+ if (status === "resolved") {
10070
+ const settled = await pollOnce(void 0, 0);
10071
+ if (settled.kind === "settled") {
10072
+ return settled;
10073
+ }
10074
+ }
10075
+ return {
10076
+ kind: "settled",
10077
+ envelope: indeterminate(
10078
+ `Cancellation of command ${command} could not be confirmed: the GM client answered "${String(status)}", so the decision had already left the cancellable state.`,
10079
+ { cancellation: "unconfirmed", status: typeof status === "string" ? status : null }
10080
+ )
10081
+ };
10082
+ }
10083
+ stderr.write(
10084
+ `Waiting for GM approval in Foundry (command ${command}, expires ${new Date(expiresAt).toISOString()}). Press Ctrl+C to request cancellation.
10085
+ `
10086
+ );
10087
+ let settleCancellation = () => {
10088
+ };
10089
+ const cancellation = new Promise((resolve7) => {
10090
+ settleCancellation = resolve7;
10091
+ });
10092
+ let cancelling = false;
10093
+ function onSignal() {
10094
+ signalScope.off("SIGINT", onSignal);
10095
+ if (cancelling) {
10096
+ return;
10097
+ }
10098
+ cancelling = true;
10099
+ onCancelRequested?.();
10100
+ void requestCancellation().then(settleCancellation);
10101
+ }
10102
+ async function raceCancellation(work) {
10103
+ return await Promise.race([work.then(() => null), cancellation]);
10104
+ }
10105
+ signalScope.on("SIGINT", onSignal);
10106
+ let pollAbort = null;
10107
+ try {
10108
+ let retryDelayMs = APPROVAL_RETRY_BASE_DELAY_MS;
10109
+ for (; ; ) {
10110
+ pollAbort = new AbortController();
10111
+ const step = await Promise.race([pollOnce(pollAbort.signal), cancellation]);
10112
+ if (step.kind === "settled") {
10113
+ return step.envelope;
10114
+ }
10115
+ if (step.kind === "pending") {
10116
+ retryDelayMs = APPROVAL_RETRY_BASE_DELAY_MS;
10117
+ const cancelled = await raceCancellation(sleep(APPROVAL_EMPTY_POLL_DELAY_MS));
10118
+ if (cancelled) {
10119
+ return cancelled.envelope;
10120
+ }
10121
+ continue;
10122
+ }
10123
+ if (now() > expiresAt + APPROVAL_RETRY_MARGIN_MS) {
10124
+ return step.envelope;
10125
+ }
10126
+ const cancelledWhileWaiting = await raceCancellation(sleep(retryDelayMs));
10127
+ if (cancelledWhileWaiting) {
10128
+ return cancelledWhileWaiting.envelope;
10129
+ }
10130
+ retryDelayMs = Math.min(retryDelayMs * 2, APPROVAL_RETRY_MAX_DELAY_MS);
10131
+ if (reconnect && step.connectionLost) {
10132
+ let failure = null;
10133
+ const cancelledWhileReconnecting = await raceCancellation(
10134
+ reconnect().catch((error) => {
10135
+ failure = error;
10136
+ })
10137
+ );
10138
+ if (cancelledWhileReconnecting) {
10139
+ return cancelledWhileReconnecting.envelope;
10140
+ }
10141
+ if (failure !== null && now() > expiresAt + APPROVAL_RETRY_MARGIN_MS) {
10142
+ return toTransportErrorEnvelope(failure);
10143
+ }
10144
+ }
10145
+ }
10146
+ } finally {
10147
+ pollAbort?.abort();
10148
+ signalScope.off("SIGINT", onSignal);
10149
+ }
10150
+ }
10151
+
8453
10152
  // src/config-io.ts
8454
10153
  function getStoredConfig(dependencies) {
8455
10154
  const existing = dependencies.configStore.readConfig();
@@ -8514,6 +10213,15 @@ var OWNERSHIP_LEVEL_LABELS = {
8514
10213
  "2": "observer",
8515
10214
  "3": "owner"
8516
10215
  };
10216
+ function renderBroadcastLines(result) {
10217
+ const requested = result?.userIds;
10218
+ return [
10219
+ `requested: ${Array.isArray(requested) ? requested.join(", ") || "(none)" : "every user"}`,
10220
+ `reached: ${(result?.activeUserIds ?? []).join(", ") || "(nobody online)"}`,
10221
+ `skipped (offline): ${(result?.inactiveUserIds ?? result?.skippedUserIds ?? []).join(", ") || "(none)"}`,
10222
+ `dispatched: ${Boolean(result?.dispatched)} \u2014 a broadcast writes nothing, so there is no state to confirm afterwards`
10223
+ ];
10224
+ }
8517
10225
  function renderCompendiumSourceLines(document) {
8518
10226
  return document?.compendiumSource ? [`compendiumSource: ${document.compendiumSource}`] : [];
8519
10227
  }
@@ -8955,6 +10663,15 @@ function renderCardsRecallLines(recall, { deleteScoped = true } = {}) {
8955
10663
  }
8956
10664
 
8957
10665
  // src/render/chat.ts
10666
+ function renderChatFlushResult(result) {
10667
+ return result?.dryRun ? [
10668
+ `[dry-run] would delete the entire chat log: ${result?.count ?? 0} message(s)`,
10669
+ `remaining after the flush: 0`
10670
+ ].join("\n") : [
10671
+ `Flushed the chat log: deleted ${result?.deleted ?? 0} of ${result?.count ?? 0} message(s)`,
10672
+ `remaining: ${result?.remaining ?? 0}`
10673
+ ].join("\n");
10674
+ }
8958
10675
  function renderChatDetails(message) {
8959
10676
  const lines = [
8960
10677
  `id: ${message?.id}`,
@@ -9212,6 +10929,13 @@ function renderFolderDetails(folder) {
9212
10929
  }
9213
10930
 
9214
10931
  // src/render/journals.ts
10932
+ function renderJournalShowResult(result) {
10933
+ return [
10934
+ `Showed journal entry ${result?.journalId} to other users`,
10935
+ `force: ${Boolean(result?.force)}`,
10936
+ ...renderBroadcastLines(result)
10937
+ ].join("\n");
10938
+ }
9215
10939
  function renderJournalDetails(journal) {
9216
10940
  const pages = Array.isArray(journal?.pages) ? journal.pages : [];
9217
10941
  const categories = Array.isArray(journal?.categories) ? journal.categories : [];
@@ -9297,6 +11021,36 @@ function renderJournalCategoryDetails(journalId, category) {
9297
11021
  }
9298
11022
 
9299
11023
  // src/render/macros.ts
11024
+ var CHAT_CAPTURE_NOTES = {
11025
+ captured: "captured (every message the macro created while the bridge waited)",
11026
+ partial: "partial (fewer messages than this macro was expected to create)",
11027
+ "not-created": "not-created (the macro created no chat message while the bridge waited)",
11028
+ unknown: "unknown (the macro may have created messages this call could not observe)"
11029
+ };
11030
+ function renderMacroExecuteResult(result) {
11031
+ if (result?.dryRun) {
11032
+ return [
11033
+ `[dry-run] would execute macro ${result?.macroId}`,
11034
+ `type: ${result?.type ?? "(unknown)"}`,
11035
+ `canExecute: ${Boolean(result?.canExecute)}`,
11036
+ `command length: ${result?.commandLength ?? 0} characters`
11037
+ ].join("\n");
11038
+ }
11039
+ const lines = [
11040
+ `Executed macro ${result?.macroId}`,
11041
+ `type: ${result?.type ?? "(unknown)"}`,
11042
+ `returned: ${JSON.stringify(result?.returned ?? null, null, 2)}`
11043
+ ];
11044
+ if (result?.returnedOmitted) {
11045
+ lines.push(`returned omitted: ${result.returnedOmitted.code} \u2014 ${result.returnedOmitted.message}`);
11046
+ }
11047
+ lines.push(
11048
+ `chat messages: ${(result?.chatMessageIds ?? []).join(", ") || "(none)"}`,
11049
+ `chat capture: ${CHAT_CAPTURE_NOTES[result?.chatCapture] ?? CHAT_CAPTURE_NOTES.unknown}`,
11050
+ "note: a macro that catches its own errors still returns null, so verify the effect with reads"
11051
+ );
11052
+ return lines.join("\n");
11053
+ }
9300
11054
  function renderMacroDetails(macro) {
9301
11055
  const lines = [
9302
11056
  `id: ${macro?.id}`,
@@ -9354,6 +11108,21 @@ function renderPlaylistSoundDetails(sound) {
9354
11108
  }
9355
11109
 
9356
11110
  // src/render/scenes.ts
11111
+ function renderSceneActivateResult(result) {
11112
+ return [
11113
+ `${result?.dryRun ? "[dry-run] would activate scene" : "Activated scene"} ${result?.sceneId}`,
11114
+ `was active: ${Boolean(result?.wasActive)}`,
11115
+ `changed: ${Boolean(result?.changed)}`
11116
+ ].join("\n");
11117
+ }
11118
+ function renderScenePullUsersResult(result) {
11119
+ return [
11120
+ `Pulled users to scene ${result?.sceneId}`,
11121
+ `pulled: ${(result?.userIds ?? []).join(", ") || "(nobody online)"}`,
11122
+ `skipped (offline): ${(result?.skippedUserIds ?? []).join(", ") || "(none)"}`,
11123
+ `dispatched: ${Boolean(result?.dispatched)} \u2014 a pull writes nothing, so there is no state to confirm afterwards`
11124
+ ].join("\n");
11125
+ }
9357
11126
  function renderSceneDetails(scene) {
9358
11127
  const lines = [
9359
11128
  `id: ${scene?.id}`,
@@ -9575,6 +11344,45 @@ function renderSettingSummaryLine(setting) {
9575
11344
  ...setting?.valueRedacted ? ["value=REDACTED"] : []
9576
11345
  ].join(" ");
9577
11346
  }
11347
+ function renderSettingValue(value) {
11348
+ return JSON.stringify(value ?? null, null, 2);
11349
+ }
11350
+ function renderSettingWriteLines(row, dryRun) {
11351
+ const verb = dryRun ? "would write" : row?.changed ? "wrote" : "left unchanged";
11352
+ return [
11353
+ `${verb}: ${row?.id ?? `${row?.namespace}.${row?.key}`} (scope: ${row?.scope ?? "?"})`,
11354
+ `previous: ${renderSettingValue(row?.previous)}`,
11355
+ `${dryRun ? "requested" : "value"}: ${renderSettingValue(row?.value)}`,
11356
+ `changed: ${Boolean(row?.changed)} / validated: ${Boolean(row?.validated)}`,
11357
+ ...row?.requiresReload ? ["requiresReload: true \u2014 Foundry acts on this only after `system reload` or a manual browser reload"] : ["requiresReload: false"]
11358
+ ];
11359
+ }
11360
+ function renderSettingWriteResult(result) {
11361
+ return renderSettingWriteLines(result, Boolean(result?.dryRun)).join("\n");
11362
+ }
11363
+ function renderSettingWriteOutcomes(result) {
11364
+ const outcomes = result?.outcomes ?? [];
11365
+ const dryRun = Boolean(result?.dryRun);
11366
+ const lines = [
11367
+ `${dryRun ? "Would write" : "Wrote"} settings (${outcomes.length}) \u2014 complete: ${Boolean(result?.complete)}`,
11368
+ ...outcomes.flatMap(
11369
+ (outcome) => outcome?.status === "updated" || outcome?.status === "unchanged" ? [
11370
+ `[${outcome.index}] ${outcome.status}`,
11371
+ ...renderSettingWriteLines(outcome, dryRun).map((line) => ` ${line}`)
11372
+ ] : [`[${outcome?.index}] ${outcome?.status} ${outcome?.id ?? ""}`]
11373
+ )
11374
+ ];
11375
+ if (result?.failure) {
11376
+ lines.push(`failure: ${result.failure.code} \u2014 ${result.failure.message}`);
11377
+ }
11378
+ return lines.join("\n");
11379
+ }
11380
+ function renderSettingBatchRows(setting) {
11381
+ return [
11382
+ renderSettingSummaryLine(setting),
11383
+ ...setting?.error ? [` error: ${setting.error.code} \u2014 ${setting.error.message}`] : [` value: ${renderSettingValue(setting?.value)}`]
11384
+ ];
11385
+ }
9578
11386
  function renderSettingDetails(setting) {
9579
11387
  return [
9580
11388
  `setting: ${setting?.id ?? `${setting?.namespace}.${setting?.key}`}`,
@@ -9684,6 +11492,38 @@ function renderUserSummaryLine(user) {
9684
11492
  `char=${user?.character ?? "-"}`
9685
11493
  ].join(" ");
9686
11494
  }
11495
+ function renderUserRoleSetResult(result) {
11496
+ const dryRun = Boolean(result?.dryRun);
11497
+ return [
11498
+ `${dryRun ? "Would set" : "Set"} role for user ${result?.userId}`,
11499
+ `role: ${result?.previousRole ?? "?"} \u2192 ${result?.role} (${result?.roleName ?? "?"})`,
11500
+ `changed: ${Boolean(result?.changed)}`
11501
+ ].join("\n");
11502
+ }
11503
+ function renderUserPermissionsSetResult(result) {
11504
+ const dryRun = Boolean(result?.dryRun);
11505
+ const overrides = Object.entries(result?.overrides ?? {});
11506
+ const lines = [
11507
+ `${dryRun ? "Would set" : "Set"} permission overrides for user ${result?.userId}`,
11508
+ `role: ${result?.role ?? "?"}`
11509
+ ];
11510
+ if (dryRun) {
11511
+ lines.push(
11512
+ "requested:",
11513
+ ...Object.entries(result?.requested ?? {}).map(
11514
+ ([name, value]) => ` ${name}: ${value === null ? "drop the override (role default applies)" : String(value)}`
11515
+ )
11516
+ );
11517
+ }
11518
+ lines.push(
11519
+ overrides.length ? "overrides:" : "overrides: (none \u2014 every permission follows the role default)",
11520
+ ...overrides.map(([name, value]) => ` ${name}: ${String(value)}`)
11521
+ );
11522
+ lines.push(
11523
+ result?.permissions ? `effective permissions: ${Object.entries(result.permissions).filter(([, granted]) => granted).map(([name]) => name).join(", ") || "(none)"}` : "effective permissions: not reported by this Foundry version"
11524
+ );
11525
+ return lines.join("\n");
11526
+ }
9687
11527
  function renderUserDetails(user) {
9688
11528
  return [
9689
11529
  `user: ${user?.name} [${user?.id}]`,
@@ -9779,6 +11619,28 @@ var RENDERERS = registerRenderers([
9779
11619
  `commands: ${(result.commands ?? []).join(", ")}`
9780
11620
  ].join("\n")
9781
11621
  ),
11622
+ detailRenderer(
11623
+ "game.pause",
11624
+ (result) => [
11625
+ `${result.dryRun ? "[dry-run] would set" : "Set"} the game to ${result.paused ? "PAUSED" : "RUNNING"} for every client`,
11626
+ `previous: ${result.previousPaused ? "paused" : "running"}`,
11627
+ `changed: ${Boolean(result.changed)}`
11628
+ ].join("\n")
11629
+ ),
11630
+ detailRenderer(
11631
+ "image.show",
11632
+ (result) => [
11633
+ `Showed image ${result.src}${result.title ? ` as "${result.title}"` : ""} to other users`,
11634
+ ...renderBroadcastLines(result)
11635
+ ].join("\n")
11636
+ ),
11637
+ detailRenderer(
11638
+ "system.reload",
11639
+ () => [
11640
+ "Reload requested: the GM browser page reloads as soon as this response is flushed.",
11641
+ "The bridge connection drops and reconnects on its own; retry the next command after a moment."
11642
+ ].join("\n")
11643
+ ),
9782
11644
  customRenderer("world.search", (result, offset) => {
9783
11645
  const results = result.results ?? [];
9784
11646
  const lines = results.length ? [
@@ -9852,6 +11714,8 @@ var RENDERERS = registerRenderers([
9852
11714
  heading: "Scenes",
9853
11715
  rows: (scene) => [`${scene.id} ${scene.name}`, ...batchOwnershipLines(scene)]
9854
11716
  }),
11717
+ detailRenderer("scene.activate", (result) => renderSceneActivateResult(result)),
11718
+ detailRenderer("scene.pull-users", (result) => renderScenePullUsersResult(result)),
9855
11719
  deleteRenderer("scene.delete", "scene", {
9856
11720
  suffix: (result) => result.wasActive ? " (was active)" : ""
9857
11721
  }),
@@ -9901,7 +11765,10 @@ var RENDERERS = registerRenderers([
9901
11765
  heading: "Users",
9902
11766
  row: (user) => renderUserSummaryLine(user)
9903
11767
  }),
9904
- detailRenderer("user.get", (result) => renderUserDetails(result.user)),
11768
+ detailRenderer(["user.get", "user.create", "user.update"], (result) => renderUserDetails(result.user)),
11769
+ deleteRenderer("user.delete", "user"),
11770
+ detailRenderer("user.role.set", (result) => renderUserRoleSetResult(result)),
11771
+ detailRenderer("user.permissions.set", (result) => renderUserPermissionsSetResult(result)),
9905
11772
  listRenderer("setting.list", {
9906
11773
  key: "settings",
9907
11774
  empty: "No registered settings found.",
@@ -9909,6 +11776,13 @@ var RENDERERS = registerRenderers([
9909
11776
  row: (setting) => renderSettingSummaryLine(setting)
9910
11777
  }),
9911
11778
  detailRenderer("setting.get", (result) => renderSettingDetails(result.setting)),
11779
+ getManyRenderer("setting.get-many", {
11780
+ key: "settings",
11781
+ heading: "Settings",
11782
+ rows: (setting) => renderSettingBatchRows(setting)
11783
+ }),
11784
+ detailRenderer("setting.set", (result) => renderSettingWriteResult(result)),
11785
+ detailRenderer("setting.set-many", (result) => renderSettingWriteOutcomes(result)),
9912
11786
  detailRenderer("actor.ownership.set", (result) => renderOwnershipSetResult("actor", result.actor)),
9913
11787
  detailRenderer("item.ownership.set", (result) => renderOwnershipSetResult("item", result.item)),
9914
11788
  detailRenderer("scene.ownership.set", (result) => renderOwnershipSetResult("scene", result.scene)),
@@ -10188,6 +12062,7 @@ var RENDERERS = registerRenderers([
10188
12062
  }
10189
12063
  }),
10190
12064
  deleteRenderer("journal.delete", "journal"),
12065
+ detailRenderer("journal.show", (result) => renderJournalShowResult(result)),
10191
12066
  listRenderer("journal.category.list", {
10192
12067
  key: "categories",
10193
12068
  empty: "No categories found.",
@@ -10233,6 +12108,7 @@ var RENDERERS = registerRenderers([
10233
12108
  rows: (m) => [`${m.id} ${m.name} ${m.type ?? ""} ${m.scope ?? ""}`, ...batchOwnershipLines(m)]
10234
12109
  }),
10235
12110
  deleteRenderer("macro.delete", "macro"),
12111
+ detailRenderer("macro.execute", (result) => renderMacroExecuteResult(result)),
10236
12112
  listRenderer("playlist.list", {
10237
12113
  key: "playlists",
10238
12114
  empty: "No playlists found.",
@@ -10291,6 +12167,7 @@ var RENDERERS = registerRenderers([
10291
12167
  }),
10292
12168
  detailRenderer(["chat.get", "chat.create"], (result) => renderChatDetails(result.message)),
10293
12169
  deleteRenderer("chat.delete", "chat message"),
12170
+ detailRenderer("chat.flush", (result) => renderChatFlushResult(result)),
10294
12171
  listRenderer("actor.list", {
10295
12172
  key: "actors",
10296
12173
  empty: "No actors found.",
@@ -10604,6 +12481,17 @@ var RENDERERS = registerRenderers([
10604
12481
  ],
10605
12482
  (result) => renderRegionBehaviorDetails(result.sceneId, result.regionId, result.behavior)
10606
12483
  ),
12484
+ detailRenderer(
12485
+ [
12486
+ "scene.region.behavior.executable.create",
12487
+ "scene.region.behavior.executable.update",
12488
+ "scene.region.behavior.executable.clone"
12489
+ ],
12490
+ (result) => [
12491
+ renderRegionBehaviorDetails(result.sceneId, result.regionId, result.behavior),
12492
+ "note: this behavior RUNS A MACRO when the region fires; system.everyone true runs it in every connected client"
12493
+ ].join("\n")
12494
+ ),
10607
12495
  detailRenderer(
10608
12496
  "scene.region.behavior.delete",
10609
12497
  (result) => result.deleted ? `Deleted region behavior ${result.id} (scene: ${result.sceneId}, region: ${result.regionId})` : `Would delete region behavior ${result.id} (scene: ${result.sceneId}, region: ${result.regionId})`
@@ -10692,40 +12580,11 @@ function humanizeCommandResult(command, result, offset = 0) {
10692
12580
  const renderer = RENDERERS[command];
10693
12581
  return renderer ? renderer(result, offset) : JSON.stringify(result, null, 2);
10694
12582
  }
10695
-
10696
- // src/exec.ts
10697
- var TRANSPORT_DETAIL_REASONS = /* @__PURE__ */ new Set([
10698
- "timeout",
10699
- "disconnected",
10700
- "invalid_json",
10701
- "unexpected_type",
10702
- "closed",
10703
- "connect_error"
10704
- ]);
10705
- function normalizeTransportMessage(message) {
10706
- if (/ECONNREFUSED/.test(message)) {
10707
- return "Could not connect to the daemon (connection refused). Is `fvtt-world-cli bridge serve` running?";
10708
- }
10709
- if (/ETIMEDOUT/.test(message)) {
10710
- return "Could not connect to the daemon (connection timed out).";
10711
- }
10712
- if (/ENOTFOUND|EAI_AGAIN/.test(message)) {
10713
- return "Could not resolve the daemon host.";
10714
- }
10715
- if (/ECONNRESET/.test(message)) {
10716
- return "The daemon connection was reset.";
10717
- }
10718
- return message;
10719
- }
10720
- function renderProtocolError(error) {
10721
- const code = error?.code ?? "UNKNOWN_ERROR";
10722
- const message = normalizeTransportMessage(String(error?.message ?? "Unknown error"));
10723
- const reason = error?.details?.reason;
10724
- const showDetails = Boolean(error?.details) && !(typeof reason === "string" && TRANSPORT_DETAIL_REASONS.has(reason));
10725
- const details = showDetails ? `
10726
- ${JSON.stringify(error.details, null, 2)}` : "";
10727
- return `${code}: ${message}${details}`;
10728
- }
12583
+
12584
+ // src/exec.ts
12585
+ function isApprovalPending(response) {
12586
+ return !response.ok && response.error?.code === ERROR_CODES.APPROVAL_PENDING;
12587
+ }
10729
12588
  async function executeRemoteCommand({
10730
12589
  commandName,
10731
12590
  params,
@@ -10752,6 +12611,22 @@ async function executeRemoteCommand({
10752
12611
  } catch (error) {
10753
12612
  response = toTransportErrorEnvelope(error);
10754
12613
  }
12614
+ if (isApprovalPending(response)) {
12615
+ response = await awaitApprovalOutcome({
12616
+ pendingResponse: response,
12617
+ send: ({ command: awaitCommand, params: awaitParams, timeoutMs: awaitTimeoutMs, signal }) => dependencies.sendCommand({
12618
+ daemonUrl: clientConfig.daemonUrl,
12619
+ deviceCredential: clientConfig.deviceCredential,
12620
+ command: awaitCommand,
12621
+ params: awaitParams,
12622
+ ...clientMaxPayloadOption(dependencies),
12623
+ timeoutMs: awaitTimeoutMs,
12624
+ ...signal ? { signal } : {}
12625
+ }),
12626
+ stderr: dependencies.stderr,
12627
+ ...typeof timeoutMs === "number" ? { timeoutMs } : {}
12628
+ });
12629
+ }
10755
12630
  if (outputJson) {
10756
12631
  write(dependencies.stdout, `${JSON.stringify(response, null, 2)}
10757
12632
  `);
@@ -10774,7 +12649,8 @@ ${humanOutput}` : humanOutput;
10774
12649
  );
10775
12650
  }
10776
12651
  }
10777
- async function processExecLine(client, rawLine, timeoutMs, dryRun) {
12652
+ async function processExecLine(session, rawLine, dryRun) {
12653
+ const timeoutMs = session.timeoutMs;
10778
12654
  let parsedLine;
10779
12655
  try {
10780
12656
  parsedLine = JSON.parse(rawLine);
@@ -10818,16 +12694,30 @@ async function processExecLine(client, rawLine, timeoutMs, dryRun) {
10818
12694
  };
10819
12695
  }
10820
12696
  const requestParams = dryRun && isWriteCommand(obj.command) ? { ...params, dryRun: true } : params;
12697
+ let envelope;
10821
12698
  try {
10822
- const envelope = await client.send({
12699
+ envelope = await session.send({
10823
12700
  command: obj.command,
10824
12701
  params: requestParams,
10825
12702
  ...typeof timeoutMs === "number" ? { timeoutMs } : {}
10826
12703
  });
10827
- return { id: callerId, envelope };
10828
12704
  } catch (error) {
10829
12705
  return { id: callerId, envelope: toTransportErrorEnvelope(error) };
10830
12706
  }
12707
+ if (!isApprovalPending(envelope)) {
12708
+ return { id: callerId, envelope };
12709
+ }
12710
+ return {
12711
+ id: callerId,
12712
+ envelope: await awaitApprovalOutcome({
12713
+ pendingResponse: envelope,
12714
+ send: (request) => session.send(request),
12715
+ reconnect: session.reconnect,
12716
+ onCancelRequested: session.onCancelRequested,
12717
+ stderr: session.stderr,
12718
+ ...typeof timeoutMs === "number" ? { timeoutMs } : {}
12719
+ })
12720
+ };
10831
12721
  }
10832
12722
  async function runExecBatch({
10833
12723
  command,
@@ -10846,17 +12736,33 @@ async function runExecBatch({
10846
12736
  const timeoutMs = typeof globalOptions.timeoutMs === "number" ? globalOptions.timeoutMs : void 0;
10847
12737
  const dryRun = Boolean(globalOptions.dryRun);
10848
12738
  const stopOnError = Boolean(command.opts().stopOnError);
12739
+ const connectOptions = {
12740
+ daemonUrl: clientConfig.daemonUrl,
12741
+ deviceCredential: clientConfig.deviceCredential,
12742
+ ...clientMaxPayloadOption(dependencies),
12743
+ ...typeof timeoutMs === "number" ? { timeoutMs } : {}
12744
+ };
10849
12745
  let client;
10850
12746
  try {
10851
- client = await connectDaemonClient({
10852
- daemonUrl: clientConfig.daemonUrl,
10853
- deviceCredential: clientConfig.deviceCredential,
10854
- ...clientMaxPayloadOption(dependencies),
10855
- ...typeof timeoutMs === "number" ? { timeoutMs } : {}
10856
- });
12747
+ client = await connectDaemonClient(connectOptions);
10857
12748
  } catch (error) {
10858
12749
  throw new ExecConnectError(toTransportErrorEnvelope(error));
10859
12750
  }
12751
+ let cancelRequested = false;
12752
+ const session = {
12753
+ send: (request) => client.send(request),
12754
+ reconnect: async () => {
12755
+ const replaced = client;
12756
+ client = await connectDaemonClient(connectOptions);
12757
+ void replaced.close().catch(() => {
12758
+ });
12759
+ },
12760
+ stderr: dependencies.stderr,
12761
+ ...typeof timeoutMs === "number" ? { timeoutMs } : {},
12762
+ onCancelRequested: () => {
12763
+ cancelRequested = true;
12764
+ }
12765
+ };
10860
12766
  const rl = createInterface({ input: stdin, crlfDelay: Infinity });
10861
12767
  let anyFailed = false;
10862
12768
  let index = 0;
@@ -10867,7 +12773,7 @@ async function runExecBatch({
10867
12773
  if (rawLine.trim() === "") {
10868
12774
  continue;
10869
12775
  }
10870
- const { id, envelope } = await processExecLine(client, rawLine, timeoutMs, dryRun);
12776
+ const { id, envelope } = await processExecLine(session, rawLine, dryRun);
10871
12777
  const outputLine = { ...envelope, index: currentIndex, ...id !== void 0 ? { id } : {} };
10872
12778
  write(dependencies.stdout, `${JSON.stringify(outputLine)}
10873
12779
  `);
@@ -10877,6 +12783,10 @@ async function runExecBatch({
10877
12783
  break;
10878
12784
  }
10879
12785
  }
12786
+ if (cancelRequested) {
12787
+ anyFailed = true;
12788
+ break;
12789
+ }
10880
12790
  }
10881
12791
  } finally {
10882
12792
  rl.close();
@@ -11090,6 +13000,73 @@ var REGION_BEHAVIOR_FIELDS = [
11090
13000
  function addRegionBehaviorFieldOptions(command, verb) {
11091
13001
  return addFieldOptions(command, verb, REGION_BEHAVIOR_FIELDS);
11092
13002
  }
13003
+ var EXECUTABLE_BEHAVIOR_FIELDS = [
13004
+ ...REGION_BEHAVIOR_FIELDS,
13005
+ {
13006
+ flag: "--macro-uuid <uuid>",
13007
+ help: {
13008
+ create: "UUID of the world macro to run (e.g. Macro.abc123); a compendium macro is refused",
13009
+ update: "UUID of the world macro to run instead",
13010
+ clone: "UUID of the world macro the clone runs"
13011
+ }
13012
+ },
13013
+ {
13014
+ flag: "--events <list>",
13015
+ help: {
13016
+ create: "Comma-separated region events that run the macro (e.g. tokenEnter,tokenExit)",
13017
+ update: "Comma-separated region events that run the macro",
13018
+ clone: "Comma-separated region events for the clone"
13019
+ },
13020
+ parser: (value) => parseCsvList(value, "--events")
13021
+ },
13022
+ {
13023
+ flag: "--everyone <everyone>",
13024
+ help: {
13025
+ create: "Run the macro on EVERY connected client instead of one elected executor (true|false)",
13026
+ update: "Run the macro on EVERY connected client instead of one elected executor (true|false)",
13027
+ clone: "Run the macro on EVERY connected client instead of one elected executor (true|false)"
13028
+ },
13029
+ parser: parseBoolean
13030
+ }
13031
+ ];
13032
+ function addExecutableBehaviorFieldOptions(command, verb) {
13033
+ return addFieldOptions(command, verb, EXECUTABLE_BEHAVIOR_FIELDS);
13034
+ }
13035
+ var USER_FIELDS = [
13036
+ {
13037
+ flag: "--color <color>",
13038
+ help: { create: "User color (e.g. #ff0000)", update: "New user color", clone: "User color override" }
13039
+ },
13040
+ {
13041
+ flag: "--pronouns <pronouns>",
13042
+ help: { create: "Pronouns shown to other players", update: "New pronouns", clone: "Pronouns override" }
13043
+ },
13044
+ {
13045
+ flag: "--avatar <avatar>",
13046
+ help: { create: "Avatar image path", update: "New avatar image path", clone: "Avatar override" }
13047
+ },
13048
+ {
13049
+ flag: "--clear-avatar",
13050
+ help: "Explicitly set avatar to null",
13051
+ conflicts: "avatar"
13052
+ },
13053
+ {
13054
+ flag: "--character <actorId>",
13055
+ help: {
13056
+ create: "Actor id of the assigned character",
13057
+ update: "Actor id of the assigned character",
13058
+ clone: "Assigned character override"
13059
+ }
13060
+ },
13061
+ {
13062
+ flag: "--clear-character",
13063
+ help: "Explicitly unassign the character (character = null)",
13064
+ conflicts: "character"
13065
+ }
13066
+ ];
13067
+ function addUserFieldOptions(command, verb) {
13068
+ return addFieldOptions(command, verb, USER_FIELDS);
13069
+ }
11093
13070
  function effectFields(transferNote) {
11094
13071
  return [
11095
13072
  {
@@ -12384,8 +14361,7 @@ function createDaemonControlRunner(dependencies) {
12384
14361
  }
12385
14362
 
12386
14363
  // src/commands/auth.ts
12387
- var AWAIT_CLIENT_TIMEOUT_FLOOR_MS = AUTH_AWAIT_PARK_CAP_MS + 5e3;
12388
- var AWAIT_EMPTY_POLL_DELAY_MS = 250;
14364
+ var AWAIT_CLIENT_TIMEOUT_FLOOR_MS = AUTH_AWAIT_PARK_CAP_MS + AWAIT_FLOOR_MARGIN_MS;
12389
14365
  var INTERACTIVE_WAIT_GUIDANCE = "Waiting for a pairing request needs an interactive terminal. Use auth pending and auth approve --yes instead.";
12390
14366
  var PRUNE_INTERACTIVE_GUIDANCE = "Non-interactive pairing prune requires --yes";
12391
14367
  var PRUNE_JSON_GUIDANCE = "Pairing prune with --json requires --yes, because the confirmation prompt is never mixed into JSON output";
@@ -12723,10 +14699,12 @@ function syncInstalledSkillCopies(dependencies) {
12723
14699
  try {
12724
14700
  const sourceDirectory = resolveSkillSourceDirectory();
12725
14701
  if (!sourceDirectory) return;
14702
+ let installedAnywhere = false;
12726
14703
  for (const root of knownSkillRoots(dependencies)) {
12727
14704
  try {
12728
14705
  const destination = join2(root, SKILL_NAME);
12729
14706
  const state = inspectInstalledSkill(destination, sourceDirectory);
14707
+ if (state !== "missing") installedAnywhere = true;
12730
14708
  if (state === "pristine") {
12731
14709
  rmSync(destination, { recursive: true, force: true });
12732
14710
  cpSync(sourceDirectory, destination, { recursive: true });
@@ -12746,6 +14724,13 @@ function syncInstalledSkillCopies(dependencies) {
12746
14724
  continue;
12747
14725
  }
12748
14726
  }
14727
+ if (!installedAnywhere) {
14728
+ write(
14729
+ dependencies.stderr,
14730
+ `The ${SKILL_NAME} agent skill is not installed. If AI agents use this CLI, consider running \`fvtt-world-cli skill install\` to give them its operating manual.
14731
+ `
14732
+ );
14733
+ }
12749
14734
  } catch {
12750
14735
  return;
12751
14736
  }
@@ -13155,6 +15140,16 @@ function registerChat({ program, dependencies }) {
13155
15140
  dependencies
13156
15141
  });
13157
15142
  });
15143
+ chat.command("flush").description(
15144
+ "Delete the ENTIRE chat log (irreversible). --dry-run reports the message count without deleting anything"
15145
+ ).action(async function flushChat() {
15146
+ await executeRemoteCommand({
15147
+ commandName: "chat.flush",
15148
+ params: {},
15149
+ command: this,
15150
+ dependencies
15151
+ });
15152
+ });
13158
15153
  chat.command("delete").requiredOption("--message-id <messageId>", "Chat message id").action(async function deleteChat(options) {
13159
15154
  await executeRemoteCommand({
13160
15155
  commandName: "chat.delete",
@@ -13606,20 +15601,93 @@ function listDocEntries(docsDirectory) {
13606
15601
  }
13607
15602
 
13608
15603
  // src/commands/discovery.ts
15604
+ var POLICY_AVAILABILITY_CODES = /* @__PURE__ */ new Set([
15605
+ ERROR_CODES.DAEMON_UNAVAILABLE,
15606
+ ERROR_CODES.BRIDGE_NOT_READY,
15607
+ ERROR_CODES.BRIDGE_DISCONNECTED
15608
+ ]);
15609
+ function commandNameSet(value) {
15610
+ return new Set(
15611
+ Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : []
15612
+ );
15613
+ }
15614
+ async function discoverPolicy(command, dependencies) {
15615
+ const clientConfig = getCommandConfig(command, dependencies);
15616
+ let response;
15617
+ try {
15618
+ response = await dependencies.sendCommand({
15619
+ daemonUrl: clientConfig.daemonUrl,
15620
+ deviceCredential: clientConfig.deviceCredential,
15621
+ command: POLICY_SNAPSHOT_COMMAND,
15622
+ params: {},
15623
+ ...clientMaxPayloadOption(dependencies),
15624
+ timeoutMs: POLICY_DISCOVERY_TIMEOUT_MS
15625
+ });
15626
+ } catch (error) {
15627
+ response = toTransportErrorEnvelope(error);
15628
+ }
15629
+ if (response.ok) {
15630
+ const snapshot = response.result ?? {};
15631
+ return {
15632
+ status: "applied",
15633
+ approve: commandNameSet(snapshot.approve),
15634
+ deny: commandNameSet(snapshot.deny)
15635
+ };
15636
+ }
15637
+ const code = response.error?.code ?? ERROR_CODES.INTERNAL_ERROR;
15638
+ if (!POLICY_AVAILABILITY_CODES.has(code)) {
15639
+ return { status: "failed", envelope: response };
15640
+ }
15641
+ return { status: "fallback", reason: code, detail: response.error?.message ?? "no reason reported" };
15642
+ }
13609
15643
  function registerDiscovery({ program, dependencies }) {
13610
- program.command("commands").description("List all protocol commands and whether each mutates world state").action(function listCommands() {
13611
- const payload = COMMAND_NAMES.map((name) => ({
13612
- command: name,
13613
- mutation: COMMAND_DEFINITIONS[name].mutation
13614
- }));
13615
- if (Boolean(this.optsWithGlobals().json)) {
13616
- write(dependencies.stdout, `${JSON.stringify(localSuccessEnvelope(payload), null, 2)}
15644
+ program.command("commands").description("List discoverable protocol commands and whether each mutates world state").action(async function listCommands() {
15645
+ const json = Boolean(this.optsWithGlobals().json);
15646
+ const discovery = await discoverPolicy(this, dependencies);
15647
+ if (discovery.status === "failed") {
15648
+ const error = discovery.envelope.error;
15649
+ if (json) {
15650
+ write(dependencies.stdout, `${JSON.stringify(discovery.envelope, null, 2)}
15651
+ `);
15652
+ } else {
15653
+ write(dependencies.stderr, `${renderProtocolError(error)}
13617
15654
  `);
15655
+ }
15656
+ throw new CommanderError7(
15657
+ exitCodeForErrorCode(error?.code),
15658
+ "fvtt-world-cli.remoteError",
15659
+ error?.message ?? "Reading the command policy failed"
15660
+ );
15661
+ }
15662
+ const applied = discovery.status === "applied";
15663
+ const payload = DISCOVERABLE_COMMAND_NAMES.filter((name) => !applied || !discovery.deny.has(name)).map(
15664
+ (name) => ({
15665
+ command: name,
15666
+ mutation: COMMAND_DEFINITIONS[name].mutation,
15667
+ ...applied && discovery.approve.has(name) ? { approval: true } : {}
15668
+ })
15669
+ );
15670
+ const policy = applied ? { applied: true, source: "bridge" } : { applied: false, source: "static", reason: discovery.reason };
15671
+ if (json) {
15672
+ write(
15673
+ dependencies.stdout,
15674
+ `${JSON.stringify({ ...localSuccessEnvelope(payload), policy }, null, 2)}
15675
+ `
15676
+ );
13618
15677
  return;
13619
15678
  }
15679
+ if (!applied) {
15680
+ write(
15681
+ dependencies.stderr,
15682
+ `warning: the command policy of the GM client holding the bridge could not be read (${discovery.reason}: ${discovery.detail}); this is the full static command registry, so denied commands are still listed and commands needing approval are not marked.
15683
+ `
15684
+ );
15685
+ }
13620
15686
  write(
13621
15687
  dependencies.stdout,
13622
- `${payload.map((entry) => `${entry.command} ${entry.mutation ? "write" : "read"}`).join("\n")}
15688
+ `${payload.map(
15689
+ (entry) => `${entry.command} ${entry.mutation ? "write" : "read"}${entry.approval ? " approval" : ""}`
15690
+ ).join("\n")}
13623
15691
  `
13624
15692
  );
13625
15693
  });
@@ -13891,6 +15959,56 @@ function registerFolder({ program, dependencies }) {
13891
15959
  });
13892
15960
  }
13893
15961
 
15962
+ // src/commands/game.ts
15963
+ function registerGame({ program, dependencies }) {
15964
+ const game = program.command("game").description("Foundry game-state commands");
15965
+ game.addHelpText(
15966
+ "after",
15967
+ "\nResult key (--json): .result.paused, .result.previousPaused and .result.changed (pause)."
15968
+ );
15969
+ game.command("pause").description("Pause or unpause the game for every connected client").requiredOption("--paused <paused>", "Target pause state (true|false)", parseBoolean).action(async function pauseGame(options) {
15970
+ await executeRemoteCommand({
15971
+ commandName: "game.pause",
15972
+ params: { paused: options.paused },
15973
+ command: this,
15974
+ dependencies
15975
+ });
15976
+ });
15977
+ }
15978
+
15979
+ // src/commands/image.ts
15980
+ function registerImage({ program, dependencies }) {
15981
+ const image = program.command("image").description("Foundry image popout commands");
15982
+ image.addHelpText(
15983
+ "after",
15984
+ [
15985
+ "",
15986
+ "Result key (--json): .result.src, .result.title, .result.userIds, .result.activeUserIds,",
15987
+ ".result.inactiveUserIds and .result.dispatched (show).",
15988
+ "",
15989
+ "`--src` takes a path inside Foundry's managed `data` source (e.g. worlds/<worldId>/art/map.webp)",
15990
+ "or an http(s) URL. A host filesystem path is refused. Showing an image is a broadcast, not a",
15991
+ "mutation: nothing is written and nothing can be confirmed afterwards."
15992
+ ].join("\n")
15993
+ );
15994
+ image.command("show").description("Pop an image out in other users' browsers").requiredOption("--src <src>", "Managed data-relative image path or http(s) URL").option("--title <title>", "Window title (default: derived by Foundry from the source)").option(
15995
+ "--user-ids <list>",
15996
+ "Comma-separated user ids to show it to (default: every user); offline users are reported as skipped",
15997
+ (value) => parseCsvList(value, "--user-ids")
15998
+ ).action(async function showImage(options) {
15999
+ await executeRemoteCommand({
16000
+ commandName: "image.show",
16001
+ params: {
16002
+ src: options.src,
16003
+ ...options.title !== void 0 ? { title: options.title } : {},
16004
+ ...options.userIds ? { userIds: options.userIds } : {}
16005
+ },
16006
+ command: this,
16007
+ dependencies
16008
+ });
16009
+ });
16010
+ }
16011
+
13894
16012
  // src/commands/item.ts
13895
16013
  function registerItem({ program, dependencies }) {
13896
16014
  const { registerOwnershipSet, registerCompendiumImport, registerBatchWriteCommands } = createSharedRegistrars(dependencies);
@@ -14228,6 +16346,27 @@ delete does NOT re-point pages (Foundry rewrites nothing either); it reports how
14228
16346
  dependencies
14229
16347
  });
14230
16348
  });
16349
+ journal.command("show").description("Open a journal entry in other users' browsers (a broadcast, not a mutation)").requiredOption("--journal-id <journalId>", "Journal entry id").addOption(
16350
+ new Option8(
16351
+ "--force <force>",
16352
+ "Push the entry even to users who cannot normally see it (true|false)"
16353
+ ).argParser(parseBoolean)
16354
+ ).option(
16355
+ "--user-ids <list>",
16356
+ "Comma-separated user ids to show it to (default: every user); offline users are reported as skipped",
16357
+ (value) => parseCsvList(value, "--user-ids")
16358
+ ).action(async function showJournal(options) {
16359
+ await executeRemoteCommand({
16360
+ commandName: "journal.show",
16361
+ params: {
16362
+ journalId: options.journalId,
16363
+ ...booleanField("force", options.force),
16364
+ ...options.userIds ? { userIds: options.userIds } : {}
16365
+ },
16366
+ command: this,
16367
+ dependencies
16368
+ });
16369
+ });
14231
16370
  registerCompendiumImport(journal, {
14232
16371
  commandName: "journal.import-from-compendium",
14233
16372
  noun: "journal entry",
@@ -14236,6 +16375,7 @@ delete does NOT re-point pages (Foundry rewrites nothing either); it reports how
14236
16375
  }
14237
16376
 
14238
16377
  // src/commands/macro.ts
16378
+ import { Option as Option9 } from "commander";
14239
16379
  function registerMacro({ program, dependencies }) {
14240
16380
  const { registerOwnershipSet, registerCompendiumImport } = createSharedRegistrars(dependencies);
14241
16381
  const macro = program.command("macro").description("Foundry macro commands");
@@ -14317,6 +16457,19 @@ function registerMacro({ program, dependencies }) {
14317
16457
  dependencies
14318
16458
  });
14319
16459
  });
16460
+ macro.command("execute").description("Run a macro in the GM client and wait for it to finish").requiredOption("--macro-id <macroId>", "Macro id").option("--actor-id <actorId>", "Actor the macro receives as its `actor`/speaker").option("--scene-id <sceneId>", "Scene the token belongs to (required with --token-id)").option("--token-id <tokenId>", "Token the macro receives as its `token` (needs --scene-id)").option("--args-json <json>", "Extra scope values as a JSON object, reachable as `scope` in the macro").addOption(
16461
+ new Option9(
16462
+ "--macro-timeout-ms <ms>",
16463
+ `How long the GM client waits for the macro, in ms (default ${MACRO_EXECUTE_TIMEOUT_DEFAULT_MS}, max ${MACRO_EXECUTE_TIMEOUT_MAX_MS}). Raise the global --timeout-ms above it, or the CLI stops waiting before MACRO_TIMEOUT can be reported.`
16464
+ ).argParser(parsePositiveInt)
16465
+ ).action(async function executeMacroCommand(options) {
16466
+ await executeRemoteCommand({
16467
+ commandName: "macro.execute",
16468
+ params: createMacroExecuteParams(options),
16469
+ command: this,
16470
+ dependencies
16471
+ });
16472
+ });
14320
16473
  registerCompendiumImport(macro, {
14321
16474
  commandName: "macro.import-from-compendium",
14322
16475
  noun: "macro",
@@ -14325,7 +16478,7 @@ function registerMacro({ program, dependencies }) {
14325
16478
  }
14326
16479
 
14327
16480
  // src/commands/playlist.ts
14328
- import { Option as Option9 } from "commander";
16481
+ import { Option as Option10 } from "commander";
14329
16482
  function registerPlaylist({ program, dependencies }) {
14330
16483
  const { registerOwnershipSet, registerCompendiumImport } = createSharedRegistrars(dependencies);
14331
16484
  const playlist = program.command("playlist").description("Foundry playlist commands");
@@ -14422,7 +16575,7 @@ function registerPlaylist({ program, dependencies }) {
14422
16575
  dependencies
14423
16576
  });
14424
16577
  });
14425
- playlist.command("play-next").requiredOption("--playlist-id <playlistId>", "Playlist id").option("--sound-id <soundId>", "Current sound id to advance from").addOption(new Option9("--direction <direction>", "1 (forward) or -1 (backward)").argParser(parseNumber)).action(async function playNextPlaylistCommand(options) {
16578
+ playlist.command("play-next").requiredOption("--playlist-id <playlistId>", "Playlist id").option("--sound-id <soundId>", "Current sound id to advance from").addOption(new Option10("--direction <direction>", "1 (forward) or -1 (backward)").argParser(parseNumber)).action(async function playNextPlaylistCommand(options) {
14426
16579
  await executeRemoteCommand({
14427
16580
  commandName: "playlist.playNext",
14428
16581
  params: {
@@ -14525,7 +16678,7 @@ function registerPlaylist({ program, dependencies }) {
14525
16678
  }
14526
16679
 
14527
16680
  // src/commands/scene.ts
14528
- import { Option as Option10 } from "commander";
16681
+ import { Option as Option11 } from "commander";
14529
16682
  function registerScene({ program, dependencies }) {
14530
16683
  const { registerOwnershipSet, registerCompendiumImport, registerBatchWriteCommands } = createSharedRegistrars(dependencies);
14531
16684
  const scene = program.command("scene").description("Foundry scene commands");
@@ -14604,11 +16757,36 @@ function registerScene({ program, dependencies }) {
14604
16757
  dependencies
14605
16758
  });
14606
16759
  });
16760
+ scene.command("activate").description("Make a scene the active one for the whole world (a no-op reports changed:false)").requiredOption("--scene-id <sceneId>", "Scene id").action(async function activateScene(options) {
16761
+ await executeRemoteCommand({
16762
+ commandName: "scene.activate",
16763
+ params: { sceneId: options.sceneId },
16764
+ command: this,
16765
+ dependencies
16766
+ });
16767
+ });
16768
+ scene.command("pull-users").description(
16769
+ "Pull users' views to a scene (a broadcast, not a mutation: only currently ONLINE users are reachable and nothing can be confirmed afterwards)"
16770
+ ).requiredOption("--scene-id <sceneId>", "Scene id").option(
16771
+ "--user-ids <list>",
16772
+ "Comma-separated user ids to pull (default: every online user); offline users are reported as skipped",
16773
+ (value) => parseCsvList(value, "--user-ids")
16774
+ ).action(async function pullUsersToScene(options) {
16775
+ await executeRemoteCommand({
16776
+ commandName: "scene.pull-users",
16777
+ params: {
16778
+ sceneId: options.sceneId,
16779
+ ...options.userIds ? { userIds: options.userIds } : {}
16780
+ },
16781
+ command: this,
16782
+ dependencies
16783
+ });
16784
+ });
14607
16785
  const sceneThumbnail = scene.command("thumbnail").description("Scene thumbnail commands");
14608
16786
  sceneThumbnail.command("generate").requiredOption("--scene-id <sceneId>", "Scene id").addOption(
14609
- new Option10("--width <width>", "Thumbnail width in px (16\u20131024, default 300)").argParser(parseNumber)
16787
+ new Option11("--width <width>", "Thumbnail width in px (16\u20131024, default 300)").argParser(parseNumber)
14610
16788
  ).addOption(
14611
- new Option10("--height <height>", "Thumbnail height in px (16\u20131024, default 100)").argParser(parseNumber)
16789
+ new Option11("--height <height>", "Thumbnail height in px (16\u20131024, default 100)").argParser(parseNumber)
14612
16790
  ).option(
14613
16791
  "--include-thumb",
14614
16792
  "Include the generated base64 data URL in the response (read it with --json; the SCENE stores the extracted asset path, reported as .thumbnail.storedPath)"
@@ -15023,7 +17201,7 @@ function registerScene({ program, dependencies }) {
15023
17201
  });
15024
17202
  registerBatchWriteCommands(sceneSound, { prefix: "scene.sound", noun: "sound" });
15025
17203
  const sceneWall = scene.command("wall").description("Scene-embedded wall commands");
15026
- addPaginationOptions(sceneWall.command("list").requiredOption("--scene-id <sceneId>", "Scene id")).addOption(new Option10("--door", "Return only doors and secret doors (door > 0)")).action(async function listSceneWalls(options) {
17204
+ addPaginationOptions(sceneWall.command("list").requiredOption("--scene-id <sceneId>", "Scene id")).addOption(new Option11("--door", "Return only doors and secret doors (door > 0)")).action(async function listSceneWalls(options) {
15027
17205
  await executeRemoteCommand({
15028
17206
  commandName: "scene.wall.list",
15029
17207
  params: {
@@ -15351,7 +17529,7 @@ function registerScene({ program, dependencies }) {
15351
17529
  const sceneRegionBehavior = sceneRegion.command("behavior").description("Region-embedded behavior commands (what a region DOES when it fires)");
15352
17530
  sceneRegionBehavior.addHelpText(
15353
17531
  "after",
15354
- '\nResult key (--json): .result.behavior (single/write) / .result.behaviors[] (list), with .result.sceneId and .result.regionId alongside.\nA behavior name may legitimately be BLANK (Foundry then displays the localized type label): pass --name "" to author one; the CLI prints it as (blank).\n--type is CREATE-ONLY, so update/clone have no --type. If a --patch-json carries a `type` key it is refused even when it restates the current type: drop the key and resend (an unchanged type is a no-op for Foundry). Changing a behavior\'s type really is impossible \u2014 for that, create the new behavior and delete the old one.\nThe code-executing types executeScript and executeMacro are REFUSED whenever you supply them, and every patch on an existing one is refused too (even --disabled): use delete to remove such a behavior, or edit it in the Foundry UI.\nclone with NO patch flags IS allowed on any behavior \u2014 note that cloning a code-executing one mints a NEW self-arming auto-trigger, so audit the region afterwards.'
17532
+ "\nResult key (--json): .result.behavior (single/write) / .result.behaviors[] (list), with .result.sceneId and .result.regionId alongside.\nA behavior name may legitimately be BLANK (Foundry then displays the localized type label): pass --name \"\" to author one; the CLI prints it as (blank).\n--type is CREATE-ONLY, so update/clone have no --type. If a --patch-json carries a `type` key it is refused even when it restates the current type: drop the key and resend (an unchanged type is a no-op for Foundry). On this family changing a behavior's type is impossible \u2014 for that, create the new behavior and delete the old one.\nThe code-executing types executeScript and executeMacro are REFUSED on THIS family whenever you supply them, and every patch on an existing one is refused too (even --disabled). executeScript is refused everywhere \u2014 it runs its source in every connected player's browser. For executeMacro use `scene region behavior executable`; delete removes either type.\nclone with NO patch flags IS allowed on any behavior \u2014 note that cloning a code-executing one mints a NEW self-arming auto-trigger, so audit the region afterwards."
15355
17533
  );
15356
17534
  addNameFilterOption(
15357
17535
  addPaginationOptions(
@@ -15422,6 +17600,44 @@ function registerScene({ program, dependencies }) {
15422
17600
  dependencies
15423
17601
  });
15424
17602
  });
17603
+ const executableBehavior = sceneRegionBehavior.command("executable").description("Region behaviors that RUN A MACRO when the region fires");
17604
+ executableBehavior.addHelpText(
17605
+ "after",
17606
+ "\nResult key (--json): .result.behavior, with .result.sceneId and .result.regionId alongside \u2014 the same shape the ordinary behavior verbs return.\nThese three verbs are the only route to an executeMacro behavior. The type is fixed: there is no --type, and the behavior cannot be turned into another type later.\n--macro-uuid must name a macro in THIS world (e.g. Macro.abc123); a compendium macro or a missing uuid is refused, so the behavior never arms a trigger that points at nothing.\n--events decides WHEN the macro runs (e.g. tokenEnter), and --everyone true runs it in EVERY connected client instead of one elected executor \u2014 read it as: every player's browser executes that macro.\nclone with no patch flags mints a second armed trigger on the same region; audit the region afterwards."
17607
+ );
17608
+ addExecutableBehaviorFieldOptions(
17609
+ addIdempotencyKeyOption(executableBehavior.command("create")).description("Arm a region with an executeMacro behavior").requiredOption("--scene-id <sceneId>", "Scene id").requiredOption("--region-id <regionId>", "Region id"),
17610
+ "create"
17611
+ ).option("--data-json <json>", "Full/extra behavior data as a JSON object (merged last)").action(async function createExecutableBehavior(options) {
17612
+ await executeRemoteCommand({
17613
+ commandName: "scene.region.behavior.executable.create",
17614
+ params: createExecutableBehaviorCreateParams(options),
17615
+ command: this,
17616
+ dependencies
17617
+ });
17618
+ });
17619
+ addExecutableBehaviorFieldOptions(
17620
+ executableBehavior.command("update").description("Edit an executeMacro behavior (the only route the ordinary update refuses)").requiredOption("--scene-id <sceneId>", "Scene id").requiredOption("--region-id <regionId>", "Region id").requiredOption("--behavior-id <behaviorId>", "Behavior id"),
17621
+ "update"
17622
+ ).option("--patch-json <json>", "Full/extra behavior patch as a JSON object (merged last)").action(async function updateExecutableBehavior(options) {
17623
+ await executeRemoteCommand({
17624
+ commandName: "scene.region.behavior.executable.update",
17625
+ params: createExecutableBehaviorUpdateParams(options),
17626
+ command: this,
17627
+ dependencies
17628
+ });
17629
+ });
17630
+ addExecutableBehaviorFieldOptions(
17631
+ addIdempotencyKeyOption(executableBehavior.command("clone")).description("Copy an executeMacro behavior into a second armed trigger").requiredOption("--scene-id <sceneId>", "Scene id").requiredOption("--region-id <regionId>", "Region id").requiredOption("--behavior-id <behaviorId>", "Source behavior id"),
17632
+ "clone"
17633
+ ).option("--patch-json <json>", "Override fields for the clone as a JSON object (merged last)").action(async function cloneExecutableBehavior(options) {
17634
+ await executeRemoteCommand({
17635
+ commandName: "scene.region.behavior.executable.clone",
17636
+ params: createExecutableBehaviorCloneParams(options),
17637
+ command: this,
17638
+ dependencies
17639
+ });
17640
+ });
15425
17641
  registerCompendiumImport(scene, {
15426
17642
  commandName: "scene.import-from-compendium",
15427
17643
  noun: "scene",
@@ -15522,19 +17738,27 @@ function registerScene({ program, dependencies }) {
15522
17738
  }
15523
17739
 
15524
17740
  // src/commands/setting.ts
17741
+ import { Option as Option12 } from "commander";
15525
17742
  function registerSetting({ program, dependencies }) {
15526
- const setting = program.command("setting").description("Foundry game settings (read-only)");
17743
+ const setting = program.command("setting").description("Foundry game settings");
15527
17744
  setting.addHelpText(
15528
17745
  "after",
15529
17746
  [
15530
17747
  "",
15531
- "Result key (--json): .result.settings[] (list, metadata only) / .result.setting (get, metadata + value).",
17748
+ "Result key (--json): .result.settings[] (list, metadata only; get-many, metadata + value) /",
17749
+ ".result.setting (get, metadata + value). A write returns the row's fields directly on .result",
17750
+ "(namespace, key, scope, previous, value, requiresReload, changed) \u2014 set-many returns .result.outcomes[].",
15532
17751
  "",
15533
17752
  "Only settings REGISTERED IN THIS SESSION are visible: a value still stored in the world by a",
15534
17753
  "disabled/uninstalled module has no registration and reports SETTING_NOT_FOUND.",
15535
17754
  "`--name` matches the namespace, the key, the raw registration name AND the localized label.",
15536
17755
  "Scope caveats: a `client`-scope value is this GM CLIENT's localStorage value, and a `user`-scope",
15537
- "value is resolved for the CURRENT GM USER \u2014 neither is ever another player's value."
17756
+ "value is resolved for the CURRENT GM USER \u2014 neither is ever another player's value.",
17757
+ "",
17758
+ "`set`/`set-many` always refuse the `fvtt-world-cli` namespace, so the bridge's own",
17759
+ "authorization settings stay beyond the CLI's reach. A write whose row reports",
17760
+ "requiresReload:true needs `system reload`",
17761
+ "(or a manual browser reload) before Foundry acts on it."
15538
17762
  ].join("\n")
15539
17763
  );
15540
17764
  addNameFilterOption(addPaginationOptions(setting.command("list"))).action(
@@ -15555,6 +17779,46 @@ function registerSetting({ program, dependencies }) {
15555
17779
  dependencies
15556
17780
  });
15557
17781
  });
17782
+ setting.command("get-many").description(
17783
+ "Read several settings in ONE call (an unknown id reports its own error, not the whole call)"
17784
+ ).requiredOption(
17785
+ "--ids <list>",
17786
+ 'Comma-separated setting ids in "namespace.key" form (e.g. core.chatBubbles,my-module.apiKey)',
17787
+ parseIdList
17788
+ ).action(async function getManySettings(options) {
17789
+ await executeRemoteCommand({
17790
+ commandName: "setting.get-many",
17791
+ params: { ids: options.ids },
17792
+ command: this,
17793
+ dependencies
17794
+ });
17795
+ });
17796
+ setting.command("set").description("Write one registered setting").requiredOption("--namespace <namespace>", 'Setting namespace (e.g. "core" or a module id)').requiredOption("--key <key>", "Setting key within the namespace").requiredOption(
17797
+ "--value-json <json>",
17798
+ 'New value as a JSON literal \u2014 true, 42, "text", [..] or {..}; a quoted string stays distinct from a boolean or a number',
17799
+ (value) => parseJsonValue(value, "--value-json")
17800
+ ).action(async function setSetting(options) {
17801
+ await executeRemoteCommand({
17802
+ commandName: "setting.set",
17803
+ params: createSettingSetParams(options),
17804
+ command: this,
17805
+ dependencies
17806
+ });
17807
+ });
17808
+ setting.command("set-many").description("Write several settings in ONE call (stops at the first element Foundry refuses)").addOption(
17809
+ new Option12(
17810
+ "--items-json <json>",
17811
+ 'Setting writes as a JSON ARRAY of {"namespace","key","value"} objects'
17812
+ ).argParser((value) => parseSettingItems(value, "--items-json"))
17813
+ ).addOption(new Option12("--items-stdin", "Read that JSON array from stdin instead").conflicts("itemsJson")).action(async function setManySettings(options) {
17814
+ const items = await resolveSettingItems(options, dependencies);
17815
+ await executeRemoteCommand({
17816
+ commandName: "setting.set-many",
17817
+ params: { items },
17818
+ command: this,
17819
+ dependencies
17820
+ });
17821
+ });
15558
17822
  }
15559
17823
 
15560
17824
  // src/commands/skill.ts
@@ -15762,10 +18026,20 @@ function registerSystem({ program, dependencies }) {
15762
18026
  dependencies
15763
18027
  });
15764
18028
  });
18029
+ system.command("reload").description(
18030
+ "Reload the GM browser page. The bridge drops and reconnects on its own; pair it with a setting whose row reports requiresReload"
18031
+ ).action(async function reload() {
18032
+ await executeRemoteCommand({
18033
+ commandName: "system.reload",
18034
+ params: {},
18035
+ command: this,
18036
+ dependencies
18037
+ });
18038
+ });
15765
18039
  }
15766
18040
 
15767
18041
  // src/commands/table.ts
15768
- import { Option as Option11 } from "commander";
18042
+ import { Option as Option13 } from "commander";
15769
18043
  function registerTable({ program, dependencies }) {
15770
18044
  const { registerOwnershipSet, registerCompendiumImport } = createSharedRegistrars(dependencies);
15771
18045
  const table = program.command("table").description("Foundry roll table commands");
@@ -15854,12 +18128,12 @@ Action verbs differ: \`table draw\` has NO .result.table \u2014 read .result.res
15854
18128
  "--idempotency-key <key>",
15855
18129
  "REQUIRED: client-supplied key so a retried draw returns the original result instead of drawing again. Reuse the SAME key across retries of one draw."
15856
18130
  ).addOption(
15857
- new Option11(
18131
+ new Option13(
15858
18132
  "--count <n>",
15859
18133
  "Number of draw ITERATIONS, max 100 (>1 uses RollTable#drawMany); each iteration can return several rows, so results are not capped at this number"
15860
18134
  ).argParser(parsePositiveInt)
15861
18135
  ).addOption(
15862
- new Option11(
18136
+ new Option13(
15863
18137
  "--roll-mode <mode>",
15864
18138
  "Chat visibility of the draw message (translated per Foundry version; default public)"
15865
18139
  ).choices([...TABLE_ROLL_MODES])
@@ -15929,7 +18203,7 @@ Result key (--json): ${TABLE_RESULT_KEY_HELP}.`);
15929
18203
  });
15930
18204
  addTableResultFieldOptions(
15931
18205
  tableResult.command("update").requiredOption("--table-id <tableId>", "Roll table id").requiredOption("--result-id <resultId>", "Result (row) id").addOption(
15932
- new Option11("--range <low,high>", "New two-integer ascending roll range, e.g. 1,3").argParser(
18206
+ new Option13("--range <low,high>", "New two-integer ascending roll range, e.g. 1,3").argParser(
15933
18207
  parseResultRange
15934
18208
  )
15935
18209
  ),
@@ -15944,7 +18218,7 @@ Result key (--json): ${TABLE_RESULT_KEY_HELP}.`);
15944
18218
  });
15945
18219
  addTableResultFieldOptions(
15946
18220
  addIdempotencyKeyOption(tableResult.command("clone")).requiredOption("--table-id <tableId>", "Roll table id").requiredOption("--result-id <resultId>", "Source result (row) id").addOption(
15947
- new Option11("--range <low,high>", "Range override (two ascending integers), e.g. 4,6").argParser(
18221
+ new Option13("--range <low,high>", "Range override (two ascending integers), e.g. 4,6").argParser(
15948
18222
  parseResultRange
15949
18223
  )
15950
18224
  ),
@@ -15973,9 +18247,23 @@ Result key (--json): ${TABLE_RESULT_KEY_HELP}.`);
15973
18247
  }
15974
18248
 
15975
18249
  // src/commands/user.ts
18250
+ import { Option as Option14 } from "commander";
15976
18251
  function registerUser({ program, dependencies }) {
15977
- const user = program.command("user").description("Foundry user commands (read-only)");
15978
- user.addHelpText("after", "\nResult key (--json): .result.user (get) / .result.users[] (list).");
18252
+ const user = program.command("user").description("Foundry user commands");
18253
+ user.addHelpText(
18254
+ "after",
18255
+ [
18256
+ "",
18257
+ "Result key (--json): .result.user (get, create, update) / .result.users[] (list). `delete`,",
18258
+ "`role set` and `permissions set` return their fields directly on .result.",
18259
+ "",
18260
+ "No user command accepts a password: Foundry sends passwords in the clear and hashes them",
18261
+ "server-side, so account credentials stay outside the bridge. Neither `role set` nor `delete`",
18262
+ "accepts the GM user the bridge itself runs through \u2014 the agent cannot demote or remove its",
18263
+ "own account.",
18264
+ "Roles are integers: 1=player, 2=trusted player, 3=assistant GM, 4=gamemaster."
18265
+ ].join("\n")
18266
+ );
15979
18267
  addNameFilterOption(addPaginationOptions(user.command("list"))).action(async function listUsers(options) {
15980
18268
  await executeRemoteCommand({
15981
18269
  commandName: "user.list",
@@ -15992,10 +18280,71 @@ function registerUser({ program, dependencies }) {
15992
18280
  dependencies
15993
18281
  });
15994
18282
  });
18283
+ addUserFieldOptions(
18284
+ addIdempotencyKeyOption(user.command("create")).description("Create a player account (no password is set)").requiredOption("--name <name>", "User name").addOption(
18285
+ new Option14(
18286
+ "--role <role>",
18287
+ "Role for the new user (1=player, 2=trusted player, 3=assistant GM, 4=gamemaster; default player)"
18288
+ ).argParser(parseUserRole)
18289
+ ),
18290
+ "create"
18291
+ ).option("--data-json <json>", "Extra user fields (e.g. flags) as a JSON object (merged last)").action(async function createUser(options) {
18292
+ await executeRemoteCommand({
18293
+ commandName: "user.create",
18294
+ params: createUserCreateParams(options),
18295
+ command: this,
18296
+ dependencies
18297
+ });
18298
+ });
18299
+ addUserFieldOptions(
18300
+ user.command("update").description("Edit a user's presentation fields (the role has its own command)").requiredOption("--user-id <userId>", "User id").option("--name <name>", "New user name"),
18301
+ "update"
18302
+ ).option("--patch-json <json>", "Extra user patch fields (e.g. flags) as a JSON object (merged last)").action(async function updateUser(options) {
18303
+ await executeRemoteCommand({
18304
+ commandName: "user.update",
18305
+ params: createUserUpdateParams(options),
18306
+ command: this,
18307
+ dependencies
18308
+ });
18309
+ });
18310
+ user.command("delete").description("Delete a user account (refuses the bridge's own GM user)").requiredOption("--user-id <userId>", "User id").action(async function deleteUser(options) {
18311
+ await executeRemoteCommand({
18312
+ commandName: "user.delete",
18313
+ params: { userId: options.userId },
18314
+ command: this,
18315
+ dependencies
18316
+ });
18317
+ });
18318
+ const userRole = user.command("role").description("User role (which capabilities the role grants)");
18319
+ userRole.command("set").description("Set a user's role (refuses the bridge's own GM user)").requiredOption("--user-id <userId>", "User id").requiredOption(
18320
+ "--role <role>",
18321
+ "New role (1=player, 2=trusted player, 3=assistant GM, 4=gamemaster)",
18322
+ parseUserRole
18323
+ ).action(async function setUserRole(options) {
18324
+ await executeRemoteCommand({
18325
+ commandName: "user.role.set",
18326
+ params: { userId: options.userId, role: options.role },
18327
+ command: this,
18328
+ dependencies
18329
+ });
18330
+ });
18331
+ const userPermissions = user.command("permissions").description("Per-user permission overrides on top of the role defaults");
18332
+ userPermissions.command("set").description("Grant, revoke or drop per-user permission overrides").requiredOption("--user-id <userId>", "User id").requiredOption(
18333
+ "--permissions-json <json>",
18334
+ 'Overrides as a JSON object, e.g. {"FILES_UPLOAD":true,"MACRO_SCRIPT":false,"BROADCAST_AUDIO":null} \u2014 true grants, false revokes, null drops the override so the role default applies again. Names are validated against the installed Foundry version.',
18335
+ parseUserPermissions
18336
+ ).action(async function setUserPermissions(options) {
18337
+ await executeRemoteCommand({
18338
+ commandName: "user.permissions.set",
18339
+ params: { userId: options.userId, permissions: options.permissionsJson },
18340
+ command: this,
18341
+ dependencies
18342
+ });
18343
+ });
15995
18344
  }
15996
18345
 
15997
18346
  // src/commands/world.ts
15998
- import { Option as Option12 } from "commander";
18347
+ import { Option as Option15 } from "commander";
15999
18348
  function registerWorld({ program, dependencies }) {
16000
18349
  const world = program.command("world").description("Foundry world-wide maintenance commands");
16001
18350
  world.addHelpText(
@@ -16037,7 +18386,7 @@ function registerWorld({ program, dependencies }) {
16037
18386
  });
16038
18387
  addPaginationOptions(
16039
18388
  world.command("search").description("Search world (and optionally compendium) documents through the bridge index").requiredOption("--query <query>", "Search query (2-256 characters; terms are AND-combined)").addOption(
16040
- new Option12(
18389
+ new Option15(
16041
18390
  "--mode <mode>",
16042
18391
  'Fields to search: "name" (stored names only) or "full" (names + body text + the Actor/Item system walk, with snippets)'
16043
18392
  ).choices([...SEARCH_MODES]).default(SEARCH_MODES[0])
@@ -16046,7 +18395,7 @@ function registerWorld({ program, dependencies }) {
16046
18395
  `Comma-separated document types to search: ${SEARCH_INDEXED_TYPES.join(",")} (default: all)`,
16047
18396
  parseSearchTypeList
16048
18397
  ).option("--include-compendia", "Also search compendium pack entry NAMES (off by default)").addOption(
16049
- new Option12(
18398
+ new Option15(
16050
18399
  "--source <source>",
16051
18400
  'Return only one section: "world", or "pack" (which requires --include-compendia)'
16052
18401
  ).choices([...SEARCH_SOURCES])
@@ -16077,7 +18426,7 @@ function createProgram(partialDependencies = {}) {
16077
18426
  "Preview a mutation without persisting it (validates, resolves the target, runs all guards). Ignored for read commands.",
16078
18427
  false
16079
18428
  ).addOption(
16080
- new Option13(
18429
+ new Option16(
16081
18430
  "--timeout-ms <ms>",
16082
18431
  "Client wait for the daemon response, in ms (default 60000). Distinct from bridge serve --request-timeout-ms."
16083
18432
  ).argParser(parsePositiveInt)
@@ -16120,6 +18469,8 @@ function createProgram(partialDependencies = {}) {
16120
18469
  registerCompendium({ program, dependencies });
16121
18470
  registerFolder({ program, dependencies });
16122
18471
  registerFile({ program, dependencies });
18472
+ registerGame({ program, dependencies });
18473
+ registerImage({ program, dependencies });
16123
18474
  applyOutputRouting(program, dependencies);
16124
18475
  applyExitOverride(program);
16125
18476
  return program;