fvtt-world-cli 1.2.0 → 1.2.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,7 @@ 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 = "1.2.0";
13
+ var PROTOCOL_VERSION = "1.2.1";
14
14
  var PROTOCOL_COMPONENTS = Object.freeze({
15
15
  MODULE: "module",
16
16
  CLI_DAEMON: "cli-daemon",
@@ -312,7 +312,10 @@ var batchIdsProperty = {
312
312
  }
313
313
  };
314
314
  var tokenIncludeProperty = {
315
- include: { type: "array", items: { type: "string", enum: ["prepared"] } }
315
+ include: { type: "array", items: { type: "string", enum: ["prepared", "delta"] } }
316
+ };
317
+ var tokenListIncludeProperty = {
318
+ include: { type: "array", items: { type: "string", enum: ["delta"] } }
316
319
  };
317
320
  var compendiumIncludeProperty = {
318
321
  include: { type: "array", items: { type: "string", enum: ["effects"] } }
@@ -1757,6 +1760,16 @@ var compendiumCommands = {
1757
1760
  ...compendiumIncludeProperty
1758
1761
  },
1759
1762
  additionalProperties: false
1763
+ }),
1764
+ "compendium.get-many": cmd({
1765
+ type: "object",
1766
+ required: ["pack", "ids"],
1767
+ properties: {
1768
+ pack: { type: "string", minLength: 1 },
1769
+ ...batchIdsProperty,
1770
+ ...compendiumIncludeProperty
1771
+ },
1772
+ additionalProperties: false
1760
1773
  })
1761
1774
  };
1762
1775
 
@@ -3223,7 +3236,8 @@ var sceneEmbeddedCommands = {
3223
3236
  properties: {
3224
3237
  sceneId: { type: "string", minLength: 1 },
3225
3238
  ...nameFilterProperty,
3226
- ...paginationProperties
3239
+ ...paginationProperties,
3240
+ ...tokenListIncludeProperty
3227
3241
  },
3228
3242
  additionalProperties: false
3229
3243
  }),
@@ -3293,6 +3307,19 @@ var sceneEmbeddedCommands = {
3293
3307
  },
3294
3308
  { mutation: true }
3295
3309
  ),
3310
+ "scene.token.restore-delta": cmd(
3311
+ {
3312
+ type: "object",
3313
+ required: ["sceneId", "tokenId"],
3314
+ properties: {
3315
+ sceneId: { type: "string", minLength: 1 },
3316
+ tokenId: { type: "string", minLength: 1 },
3317
+ ...dryRunProperty
3318
+ },
3319
+ additionalProperties: false
3320
+ },
3321
+ { mutation: true }
3322
+ ),
3296
3323
  ...batchWriteCommands("scene.token", {
3297
3324
  scopeProperties: sceneScopeProperties,
3298
3325
  createSchema: tokenCreateSchema,
@@ -5356,6 +5383,7 @@ var DEFAULT_COMMAND_PROFILE = deepFreeze({
5356
5383
  "scene.token.update": "allow",
5357
5384
  "scene.token.delete": "approve",
5358
5385
  "scene.token.clone": "allow",
5386
+ "scene.token.restore-delta": "approve",
5359
5387
  "scene.token.create-many": "allow",
5360
5388
  "scene.token.update-many": "allow",
5361
5389
  "scene.token.delete-many": "approve",
@@ -5653,6 +5681,7 @@ var DEFAULT_COMMAND_PROFILE = deepFreeze({
5653
5681
  "compendium.list": "allow",
5654
5682
  "compendium.index": "allow",
5655
5683
  "compendium.get": "allow",
5684
+ "compendium.get-many": "allow",
5656
5685
  "folder.list": "allow",
5657
5686
  "folder.get": "allow",
5658
5687
  "folder.create": "allow",
@@ -5682,7 +5711,12 @@ var DEFAULT_COMMAND_PROFILE = deepFreeze({
5682
5711
 
5683
5712
  // ../protocol/src/command-risk.js
5684
5713
  var DESTRUCTIVE_VERBS = Object.freeze(["delete", "delete-many"]);
5685
- var DESTRUCTIVE_COMMANDS = Object.freeze(["chat.flush", "file.move", "scene.fog.reset"]);
5714
+ var DESTRUCTIVE_COMMANDS = Object.freeze([
5715
+ "chat.flush",
5716
+ "file.move",
5717
+ "scene.fog.reset",
5718
+ "scene.token.restore-delta"
5719
+ ]);
5686
5720
  var APPROVE_EXTRA_COMMANDS = Object.freeze(["system.reload", "user.create"]);
5687
5721
  var DENIED_BY_DEFAULT_COMMANDS = Object.freeze([
5688
5722
  "macro.execute",
@@ -8833,7 +8867,10 @@ function parseActorIncludeFields(value) {
8833
8867
  ]);
8834
8868
  }
8835
8869
  function parseTokenIncludeFields(value) {
8836
- return parseAllowedIncludeFields(value, ["prepared"]);
8870
+ return parseAllowedIncludeFields(value, ["prepared", "delta"]);
8871
+ }
8872
+ function parseTokenListIncludeFields(value) {
8873
+ return parseAllowedIncludeFields(value, ["delta"]);
8837
8874
  }
8838
8875
  function parseSystemInfoIncludeFields(value) {
8839
8876
  return parseAllowedIncludeFields(value, ["modules"]);
@@ -10366,8 +10403,11 @@ function renderBroadcastLines(result) {
10366
10403
  `dispatched: ${Boolean(result?.dispatched)} \u2014 a broadcast writes nothing, so there is no state to confirm afterwards`
10367
10404
  ];
10368
10405
  }
10369
- function renderCompendiumSourceLines(document) {
10370
- return document?.compendiumSource ? [`compendiumSource: ${document.compendiumSource}`] : [];
10406
+ function renderProvenanceLines(document) {
10407
+ return [
10408
+ ...document?.compendiumSource ? [`compendiumSource: ${document.compendiumSource}`] : [],
10409
+ ...document?.duplicateSource ? [`duplicateSource: ${document.duplicateSource}`] : []
10410
+ ];
10371
10411
  }
10372
10412
  function renderOwnershipLines(ownership) {
10373
10413
  if (!ownership || typeof ownership !== "object") {
@@ -10479,7 +10519,7 @@ function renderItemDetails(item, prefixLines = []) {
10479
10519
  if (item && Object.hasOwn(item, "effects")) {
10480
10520
  lines.push(`effects: ${JSON.stringify(item.effects ?? [], null, 2)}`);
10481
10521
  }
10482
- lines.push(...renderCompendiumSourceLines(item));
10522
+ lines.push(...renderProvenanceLines(item));
10483
10523
  if (item && Object.hasOwn(item, "ownership")) {
10484
10524
  lines.push(...renderOwnershipLines(item.ownership));
10485
10525
  }
@@ -10505,7 +10545,7 @@ function renderActorDetails(actor) {
10505
10545
  if (actor && Object.hasOwn(actor, "effects")) {
10506
10546
  lines.push(`effects: ${JSON.stringify(actor.effects ?? [], null, 2)}`);
10507
10547
  }
10508
- lines.push(...renderCompendiumSourceLines(actor));
10548
+ lines.push(...renderProvenanceLines(actor));
10509
10549
  if (actor && Object.hasOwn(actor, "ownership")) {
10510
10550
  lines.push(...renderOwnershipLines(actor.ownership));
10511
10551
  }
@@ -10551,7 +10591,7 @@ function renderCardsDetails(cards) {
10551
10591
  `cards: ${rows.length}`,
10552
10592
  ...rows.map((card) => ` ${renderCardLine(card)}`)
10553
10593
  ];
10554
- lines.push(...renderCompendiumSourceLines(cards));
10594
+ lines.push(...renderProvenanceLines(cards));
10555
10595
  if (cards && Object.hasOwn(cards, "ownership")) {
10556
10596
  lines.push(...renderOwnershipLines(cards.ownership));
10557
10597
  }
@@ -11090,7 +11130,7 @@ function renderJournalDetails(journal) {
11090
11130
  `sort: ${journal?.sort ?? 0}`,
11091
11131
  `pages: ${pages.length}`
11092
11132
  ];
11093
- lines.push(...renderCompendiumSourceLines(journal));
11133
+ lines.push(...renderProvenanceLines(journal));
11094
11134
  if (journal && Object.hasOwn(journal, "ownership")) {
11095
11135
  lines.push(...renderOwnershipLines(journal.ownership));
11096
11136
  }
@@ -11206,7 +11246,7 @@ function renderMacroDetails(macro) {
11206
11246
  `flags: ${JSON.stringify(macro?.flags ?? {}, null, 2)}`,
11207
11247
  `command: ${macro?.command ?? ""}`
11208
11248
  ];
11209
- lines.push(...renderCompendiumSourceLines(macro));
11249
+ lines.push(...renderProvenanceLines(macro));
11210
11250
  if (macro && Object.hasOwn(macro, "ownership")) {
11211
11251
  lines.push(...renderOwnershipLines(macro.ownership));
11212
11252
  }
@@ -11229,7 +11269,7 @@ function renderPlaylistDetails(playlist) {
11229
11269
  `flags: ${JSON.stringify(playlist?.flags ?? {}, null, 2)}`,
11230
11270
  `sounds: ${playlist?.sounds?.length ?? 0}`
11231
11271
  ];
11232
- lines.push(...renderCompendiumSourceLines(playlist));
11272
+ lines.push(...renderProvenanceLines(playlist));
11233
11273
  if (playlist && Object.hasOwn(playlist, "ownership")) {
11234
11274
  lines.push(...renderOwnershipLines(playlist.ownership));
11235
11275
  }
@@ -11288,7 +11328,7 @@ function renderSceneDetails(scene) {
11288
11328
  for (const level of Array.isArray(scene?.levels) ? scene.levels : []) {
11289
11329
  lines.push(`level: ${renderLevelRow(level)}`);
11290
11330
  }
11291
- lines.push(...renderCompendiumSourceLines(scene));
11331
+ lines.push(...renderProvenanceLines(scene));
11292
11332
  if (scene && Object.hasOwn(scene, "ownership")) {
11293
11333
  lines.push(...renderOwnershipLines(scene.ownership));
11294
11334
  }
@@ -11328,11 +11368,29 @@ function renderTokenDetails(sceneId, token) {
11328
11368
  `disposition: ${token?.disposition ?? ""}`,
11329
11369
  `texture: ${JSON.stringify(token?.texture ?? null)}`
11330
11370
  ];
11371
+ if (token && Object.hasOwn(token, "delta")) {
11372
+ lines.push(`delta: ${token.delta === null ? "none" : "stored (use --json for values)"}`);
11373
+ }
11331
11374
  if (token && Object.hasOwn(token, "prepared")) {
11332
11375
  lines.push(`prepared: ${Object.keys(token.prepared ?? {}).join(", ")} (use --json for values)`);
11333
11376
  }
11334
11377
  return lines.join("\n");
11335
11378
  }
11379
+ function tokenDeltaSummaryCell(token) {
11380
+ if (!token || !Object.hasOwn(token, "deltaSummary")) return "";
11381
+ const summary = token.deltaSummary;
11382
+ if (summary === null) return " overrides=none";
11383
+ return ` overrides=${(summary.overriddenKeys ?? []).join(",")} (item entries ${summary.itemEntries}, effect entries ${summary.effectEntries})`;
11384
+ }
11385
+ function renderTokenDeltaRestore(result) {
11386
+ const discarded = result.discarded;
11387
+ const verb = result.dryRun ? "[dry-run] would discard" : "Discarded";
11388
+ return [
11389
+ `scene: ${result.sceneId}`,
11390
+ `token: ${result.tokenId}`,
11391
+ result.changed ? `${verb} token overrides: ${(discarded?.overriddenKeys ?? []).join(", ")} (item entries ${discarded?.itemEntries ?? 0}, effect entries ${discarded?.effectEntries ?? 0})` : "No token-level overrides; nothing to restore."
11392
+ ].join("\n");
11393
+ }
11336
11394
  function tokenItemPrefix(result) {
11337
11395
  const lines = [
11338
11396
  `scene: ${result.sceneId}`,
@@ -11617,7 +11675,7 @@ function renderTableDetails(table) {
11617
11675
  `results: ${results.length}`,
11618
11676
  ...results.map((result) => ` ${renderTableResultLine(result)}`)
11619
11677
  ];
11620
- lines.push(...renderCompendiumSourceLines(table));
11678
+ lines.push(...renderProvenanceLines(table));
11621
11679
  if (table && Object.hasOwn(table, "ownership")) {
11622
11680
  lines.push(...renderOwnershipLines(table.ownership));
11623
11681
  }
@@ -12407,6 +12465,17 @@ var RENDERERS = registerRenderers([
12407
12465
  `document: ${JSON.stringify(result.document ?? null, null, 2)}`
12408
12466
  ].join("\n")
12409
12467
  ),
12468
+ customRenderer("compendium.get-many", (result) => {
12469
+ const entries = result.entries ?? [];
12470
+ return [
12471
+ `pack: ${result.pack}`,
12472
+ `Entries (${entries.length})`,
12473
+ ...entries.map(
12474
+ (entry) => `${entry.entryId} ${entry.documentName ?? ""} ${entry.document?.name ?? ""}${Array.isArray(entry.effects) ? ` effects=${entry.effects.length}` : ""}`
12475
+ ),
12476
+ "(use --json for the documents)"
12477
+ ].join("\n");
12478
+ }),
12410
12479
  listRenderer("folder.list", {
12411
12480
  key: "folders",
12412
12481
  empty: "No folders found.",
@@ -12540,13 +12609,14 @@ var RENDERERS = registerRenderers([
12540
12609
  empty: "No tokens found.",
12541
12610
  heading: "Tokens",
12542
12611
  prefix: scenePrefix,
12543
- row: (token) => `${token.id} ${token.name} actor=${token.actorId ?? "-"} linked=${String(Boolean(token.actorLink))} (${token.x},${token.y})${levelMembershipCell(token)}`
12612
+ row: (token) => `${token.id} ${token.name} actor=${token.actorId ?? "-"} linked=${String(Boolean(token.actorLink))} (${token.x},${token.y})${levelMembershipCell(token)}${tokenDeltaSummaryCell(token)}`
12544
12613
  }),
12545
12614
  detailRenderer(
12546
12615
  ["scene.token.get", "scene.token.create", "scene.token.update", "scene.token.clone"],
12547
12616
  (result) => renderTokenDetails(result.sceneId, result.token)
12548
12617
  ),
12549
12618
  deleteRenderer("scene.token.delete", "token", { suffix: sceneSuffix }),
12619
+ detailRenderer("scene.token.restore-delta", (result) => renderTokenDeltaRestore(result)),
12550
12620
  listRenderer("scene.token.item.list", {
12551
12621
  key: "items",
12552
12622
  empty: "No items found.",
@@ -15684,11 +15754,18 @@ function registerCombat({ program, dependencies }) {
15684
15754
  }
15685
15755
 
15686
15756
  // src/commands/compendium.ts
15757
+ function addCompendiumIncludeOption(command) {
15758
+ return command.option(
15759
+ "--include <fields>",
15760
+ "Comma-separated extra fields to include (allowed: effects)",
15761
+ parseCompendiumIncludeFields
15762
+ );
15763
+ }
15687
15764
  function registerCompendium({ program, dependencies }) {
15688
15765
  const compendium = program.command("compendium").description("Foundry compendium pack commands (read)");
15689
15766
  compendium.addHelpText(
15690
15767
  "after",
15691
- "\nResult key (--json): .result.packs[] (list), .result.entries[] (index), .result.document (get \u2014 Foundry's raw source object)."
15768
+ "\nResult key (--json): .result.packs[] (list), .result.entries[] (index), .result.document (get \u2014 Foundry's raw source object plus id, compendiumSource and duplicateSource), .result.entries[] (get-many \u2014 one get result per requested id, in request order)."
15692
15769
  );
15693
15770
  addPaginationOptions(compendium.command("list")).action(async function listCompendiums(options) {
15694
15771
  await executeRemoteCommand({
@@ -15718,10 +15795,8 @@ function registerCompendium({ program, dependencies }) {
15718
15795
  dependencies
15719
15796
  });
15720
15797
  });
15721
- compendium.command("get").requiredOption("--pack <pack>", "Compendium pack id").requiredOption("--entry-id <entryId>", "Compendium entry id").option(
15722
- "--include <fields>",
15723
- "Comma-separated extra fields to include (allowed: effects)",
15724
- parseCompendiumIncludeFields
15798
+ addCompendiumIncludeOption(
15799
+ compendium.command("get").requiredOption("--pack <pack>", "Compendium pack id").requiredOption("--entry-id <entryId>", "Compendium entry id")
15725
15800
  ).action(async function getCompendiumEntry(options) {
15726
15801
  await executeRemoteCommand({
15727
15802
  commandName: "compendium.get",
@@ -15734,6 +15809,20 @@ function registerCompendium({ program, dependencies }) {
15734
15809
  dependencies
15735
15810
  });
15736
15811
  });
15812
+ addCompendiumIncludeOption(
15813
+ compendium.command("get-many").description("Read several entries of one compendium pack in a single call").requiredOption("--pack <pack>", "Compendium pack id").requiredOption("--ids <list>", "Comma-separated entry ids (atomic: all must exist)", parseIdList)
15814
+ ).action(async function getManyCompendiumEntries(options) {
15815
+ await executeRemoteCommand({
15816
+ commandName: "compendium.get-many",
15817
+ params: {
15818
+ pack: options.pack,
15819
+ ids: options.ids,
15820
+ ...options.include ? { include: options.include } : {}
15821
+ },
15822
+ command: this,
15823
+ dependencies
15824
+ });
15825
+ });
15737
15826
  }
15738
15827
 
15739
15828
  // src/commands/config.ts
@@ -17028,17 +17117,26 @@ function registerScene({ program, dependencies }) {
17028
17117
  const sceneToken = scene.command("token").description("Scene-embedded token commands");
17029
17118
  addNameFilterOption(
17030
17119
  addPaginationOptions(sceneToken.command("list").requiredOption("--scene-id <sceneId>", "Scene id"))
17120
+ ).option(
17121
+ "--include <fields>",
17122
+ "Comma-separated extra fields to include (allowed: delta \u2014 a derived per-token summary of the token-level overrides, null when there are none)",
17123
+ parseTokenListIncludeFields
17031
17124
  ).action(async function listSceneTokens(options) {
17032
17125
  await executeRemoteCommand({
17033
17126
  commandName: "scene.token.list",
17034
- params: { sceneId: options.sceneId, ...nameFilterParams(options), ...paginationParams(options) },
17127
+ params: {
17128
+ sceneId: options.sceneId,
17129
+ ...nameFilterParams(options),
17130
+ ...paginationParams(options),
17131
+ ...options.include ? { include: options.include } : {}
17132
+ },
17035
17133
  command: this,
17036
17134
  dependencies
17037
17135
  });
17038
17136
  });
17039
17137
  sceneToken.command("get").requiredOption("--scene-id <sceneId>", "Scene id").requiredOption("--token-id <tokenId>", "Token id").option(
17040
17138
  "--include <fields>",
17041
- "Comma-separated extra fields to include (allowed: prepared)",
17139
+ "Comma-separated extra fields to include (allowed: prepared, delta \u2014 the stored ActorDelta source, null for a linked token or when none is stored)",
17042
17140
  parseTokenIncludeFields
17043
17141
  ).action(async function getSceneToken(options) {
17044
17142
  await executeRemoteCommand({
@@ -17093,6 +17191,16 @@ function registerScene({ program, dependencies }) {
17093
17191
  dependencies
17094
17192
  });
17095
17193
  });
17194
+ sceneToken.command("restore-delta").description(
17195
+ "Discard every token-level override of an unlinked token (items, effects, system data, name, \u2026) so it follows its world actor again"
17196
+ ).requiredOption("--scene-id <sceneId>", "Scene id").requiredOption("--token-id <tokenId>", "Unlinked token id").action(async function restoreSceneTokenDelta(options) {
17197
+ await executeRemoteCommand({
17198
+ commandName: "scene.token.restore-delta",
17199
+ params: { sceneId: options.sceneId, tokenId: options.tokenId },
17200
+ command: this,
17201
+ dependencies
17202
+ });
17203
+ });
17096
17204
  registerBatchWriteCommands(sceneToken, {
17097
17205
  prefix: "scene.token",
17098
17206
  noun: "token",
package/docs/commands.md CHANGED
@@ -17,8 +17,9 @@ becomes `scene token get`. Nested commands require the parent IDs as well as the
17
17
 
18
18
  With a bridge connected, discovery omits denied commands and marks approval requirements with
19
19
  `approval: true`. If the daemon or bridge is unavailable, it returns the static registry with
20
- `policy.applied: false`. Authentication and protocol errors do not trigger that fallback.
21
- `schema` and `--help` always describe the static registry, including denied commands.
20
+ `policy.applied: false`; `policy` sits at the top level of the response, beside `result`.
21
+ Authentication and protocol errors do not trigger that fallback. `schema` and `--help` always
22
+ describe the static registry, including denied commands.
22
23
 
23
24
  ## Capability map
24
25
 
@@ -164,13 +165,15 @@ Use JSON flags such as `--data-json` and `--patch-json` for structured values. U
164
165
 
165
166
  - Nested objects merge recursively.
166
167
  - Ordinary arrays replace as a whole.
167
- - Dotted paths target object properties where permitted; dotted writes inside arrays are refused.
168
- - Foundry deletion syntax can remove permitted nested keys.
168
+ - Dotted paths target object properties where permitted; a top-level dotted write into an array
169
+ field, such as `c.0`, is refused.
170
+ - Foundry's operator keys work at any permitted nested level: `{"-=key": null}` deletes the key,
171
+ and `{"==key": value}` replaces the key's whole value instead of merging into it.
169
172
  - Embedded collections follow their family's Foundry semantics.
170
173
 
171
174
  Read arrays before editing and preserve unchanged values. For a wall's `c` field, send the complete
172
- `[x1, y1, x2, y2]`, not `c.0`. Invalid or ambiguous patch shapes return `INVALID_PARAMS` before
173
- mutation, including in bulk writes and dry runs. Correct the fields identified by the error.
175
+ `[x1, y1, x2, y2]`, not `c.0`. Array writes that Foundry would discard return `INVALID_PARAMS`
176
+ before mutation, including in bulk writes and dry runs. Correct the fields identified by the error.
174
177
 
175
178
  Prefer dedicated commands for creating embedded documents. A parent update that adds entries
176
179
  without `_id`, such as new `behaviors` in `scene.region.update`, can create them but return an
@@ -190,6 +193,12 @@ A timeout or disconnect can happen after a write commits. An update error with `
190
193
  or `details.indeterminate` can also mean that some changes persisted. Read affected state before
191
194
  retrying and send only what remains to be done.
192
195
 
196
+ An update whose stored state differs from the request fails with `INTERNAL_ERROR`, and
197
+ `details.paths` lists the differing paths with the requested and the stored value. Foundry core, the
198
+ game system or a module may have changed the value; Foundry stores a raw `&` in rich text as
199
+ `&amp;`, for example. The bridge cannot tell which, so the caller judges from the values. Sending
200
+ the same patch again stores the same result.
201
+
193
202
  An idempotency key identifies one logical request and can prevent duplicate effects while its
194
203
  cached state survives. Reuse it only for the same request; a corrected or remaining patch is a
195
204
  new operation and needs a fresh key. Follow the [delivery rules](protocol.md#delivery-states-and-retries)
@@ -11,6 +11,7 @@ The bridge supports Foundry VTT v13 and v14. Check your connected version with
11
11
  | Scene thumbnail rendering | Renders the scene | Renders the scene's initial level by default | On v14, expect the initial level in the thumbnail. Check the returned dimensions and image. |
12
12
  | Scene thumbnail files | Uses a stable scene-based filename | Uses a filename based on the image content | Use the returned path. On v14, a changed image can leave an older thumbnail file behind. |
13
13
  | User permission names | `TEMPLATE_CREATE` | `REGION_CREATE` | Use the permission keys reported by the connected Foundry version when calling `user.permissions.set`. |
14
+ | Legacy update operators `-=key` (deletion) and `==key` (forced replacement) | Work without a warning | Work, and the GM browser console logs a deprecation warning for each such key, stating that support will be removed in Version 16 | Nothing changes for v13 or v14 writes. The bridge passes the operator keys through without translating them to Foundry 14's operator objects, so they will stop working once Foundry removes them. |
14
15
 
15
16
  Scene placeable fields and available region behavior types also differ. Read the target document
16
17
  before preparing a patch; valid data from one version is not automatically valid on the other.
package/docs/protocol.md CHANGED
@@ -181,6 +181,9 @@ filters apply before pagination. Previewed documents have no persistent identity
181
181
  observed during a preview must not be reused. Some result fields depend on the connected Foundry
182
182
  version; [Foundry compatibility](compatibility.md) lists them.
183
183
 
184
+ World document reads carry `compendiumSource` and `duplicateSource`, hoisted from Foundry's
185
+ `_stats` and `null` when absent.
186
+
184
187
  Broadcast actions report `dispatched` and target users rather than a confirmed player-side result.
185
188
  The active/inactive split is included where knowable.
186
189
 
@@ -196,10 +199,12 @@ world state when the return value does not establish the effect.
196
199
 
197
200
  ### Setting results
198
201
 
199
- Setting writes return the observed `value` and its `previous` value. Registered types or callbacks
200
- may normalize the input. Confirmation establishes that the previous value changed, not that the
201
- requested value was stored exactly. Writing an already-stored value succeeds as unchanged without
202
- calling Foundry. `requiresReload: true` indicates that the GM client needs a reload.
202
+ Setting writes return the observed `value` and its `previous` value, as `game.settings.get`
203
+ returns them after the registered type's cast. Confirmation compares the JSON Foundry stores with
204
+ the JSON of the validated request; a different stored value, for example one rewritten by an
205
+ `onChange` callback, is a mismatch error with a `paths` entry for `value`. Writing an
206
+ already-stored value succeeds as unchanged without calling Foundry. `requiresReload: true`
207
+ indicates that the GM client needs a reload.
203
208
 
204
209
  ## Write confirmation
205
210
 
@@ -207,11 +212,24 @@ Document writes report success only after confirmation. Foundry can resolve a ve
207
212
  write without throwing, and hooks can remove or rewrite part of a patch. Single and bulk updates
208
213
  pass Foundry a private copy, then compare stored state with the original request.
209
214
 
215
+ Confirmation compares each requested leaf path that survives Foundry's cleaning with the stored
216
+ source. Foundry drops a key the document's schema does not declare without an error before that
217
+ comparison, so a write made only of undeclared keys reports success with nothing stored. A deletion (`-=key` or
218
+ Foundry 14's `ForcedDeletion`) is confirmed when the key is absent. A forced replacement (`==key`
219
+ or Foundry 14's `ForcedReplacement`) is confirmed when the whole stored value at that path equals the requested
220
+ value after Foundry's cleaning.
221
+
210
222
  A valid patch whose requested state is already stored remains a successful no-op. A validation
211
223
  failure recovered after an unwritten update returns `INVALID_PARAMS`. Unconfirmed, partial, or
212
224
  indeterminate updates can return `INTERNAL_ERROR` with these details:
213
225
 
214
- - `fields`: requested fields whose state was not confirmed.
226
+ - `fields`: requested top-level fields whose state was not confirmed.
227
+ - `paths`: up to 20 entries `{ path, requested, stored }`, one per leaf path where the stored value
228
+ differs from the request. `absent: "stored"` replaces `stored` when the key is missing from the
229
+ stored source, and `absent: "requested"` replaces `requested` for a deletion that did not land.
230
+ A long string, or an object or array whose JSON is long, is shortened to a window around the
231
+ first differing character; `truncated` then carries `format` (`text` or `json`), `offset`, and
232
+ the full `requestedLength` and `storedLength`. `pathsTruncated` counts the entries beyond 20.
215
233
  - `partial: true`: stored data changed in a requested field, but the full requested state was
216
234
  not reached. A hook may have applied only part of a nested object or replaced a value.
217
235
  - `changedFields`: requested top-level fields observed to change, including fields that changed
@@ -221,6 +239,12 @@ indeterminate updates can return `INTERNAL_ERROR` with these details:
221
239
  - `indeterminate: true`: confirmation could not establish the outcome. Some or all of the write
222
240
  may have persisted. `changedFields` is `null` when the before/after comparison was unavailable.
223
241
 
242
+ When stored differs from requested, the message says that the bridge cannot verify whether Foundry
243
+ core, the game system or a module normalized the value, or whether a hook rewrote, dropped or vetoed
244
+ the change. A blocking module is listed as one possible cause, never as the diagnosis. Resending the
245
+ same patch does not change the result. Every confirmed write that can report a mismatch carries the
246
+ same `paths`, and so does a `dropped` bulk outcome when the stored state was compared.
247
+
224
248
  Parent patches that create embedded entries without `_id`, such as new `behaviors` in
225
249
  `scene.region.update`, return an indeterminate error after writing because parent update results
226
250
  cannot confirm those creations. A retry may duplicate them. Read the embedded collection first,
@@ -231,10 +255,10 @@ new operation with a fresh idempotency key if using one.
231
255
 
232
256
  ### Patch shape checks
233
257
 
234
- Single updates, bulk updates, and dry runs reject ambiguous field spellings, invalid or discarded
235
- array values, and dotted writes inside arrays with `INVALID_PARAMS` before mutation. Recognized
236
- legacy fields are checked against their migrated destinations as well. Send whole arrays; ordinary
237
- dotted object-property writes remain supported where the schema permits them.
258
+ Single updates, bulk updates, and dry runs reject invalid or discarded array values, top-level
259
+ dotted writes into array fields, and writes to legacy fields the running core moved, with
260
+ `INVALID_PARAMS` before mutation. Send whole arrays; ordinary dotted object-property writes remain
261
+ supported where the schema permits them. Other key spellings reach Foundry as written.
238
262
 
239
263
  Errors identify the `field` and may include `arrayField`, `requested`, `stored`, or migration
240
264
  destination details. Bulk failures add the entry's `index` and `id`; single failures use document
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fvtt-world-cli",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Safe local CLI and authenticated bridge for automating live Foundry VTT worlds.",
5
5
  "keywords": [
6
6
  "foundry-vtt",
@@ -56,6 +56,6 @@
56
56
  "zod": "^3.24.3"
57
57
  },
58
58
  "devDependencies": {
59
- "@fvtt-world-cli/protocol": "1.2.0"
59
+ "@fvtt-world-cli/protocol": "1.2.1"
60
60
  }
61
61
  }
@@ -80,6 +80,9 @@ level ids in a preview are provisional.
80
80
  Deleting a level also deletes its tokens and every placeable only on that level, and the dry run
81
81
  does not list them: read the scene's placeables (token `level`, others' `levels`) and tell the user
82
82
  what the delete removes before running it.
83
+ An unlinked token keeps its own overrides, which edits to its world actor never reach. Before
84
+ editing an actor that has placed tokens, read its tokens' overrides and tell the user which tokens
85
+ the edit will not change.
83
86
  `setting.set` cannot touch this module's own namespace (`SETTING_PROTECTED` is final; never retry).
84
87
  When a setting write returns `requiresReload: true`, the change needs a GM-client reload to take
85
88
  effect; `system.reload` performs it, drops the bridge, and requires a reconnect wait before the
@@ -92,12 +95,13 @@ signal, not a failure — reconnect and continue rather than retrying the reload
92
95
  Discover → inspect schema → locate → read → smallest patch → dry-run → commit → verify. Never
93
96
  guess a command name or parameter.
94
97
 
95
- 1. Run `fvtt-world-cli commands --json`. Continue only when `policy.applied` is `true` and the
96
- required command is present. A `false` value means no bridge answered and the output is the static
97
- registry, not the client's permissions. Report an absent command instead of seeking another way
98
- to produce the same effect. Warn the user before a command marked `"approval": true`, and prefer
99
- one `*-many` command when it can replace several approvals. Dotted protocol names map to spaced
100
- CLI subcommands, such as `actor.item.update` to `actor item update`.
98
+ 1. Run `fvtt-world-cli commands --json`. Continue only when `policy.applied` (top level, beside
99
+ `result`) is `true` and the required command is present. A `false` value means no bridge
100
+ answered and the output is the static registry, not the client's permissions. Report an absent
101
+ command instead of seeking another way to produce the same effect. Warn the user before a
102
+ command marked `"approval": true`, and prefer one `*-many` command when it can replace several
103
+ approvals. Dotted protocol names map to spaced CLI subcommands, such as `actor.item.update` to
104
+ `actor item update`.
101
105
  2. `fvtt-world-cli schema <command>` shows the exact request parameters, required fields, enums,
102
106
  and whether unknown fields are accepted; `fvtt-world-cli <command path> --help` maps flags.
103
107
  3. Locate targets with the narrowest query: a family `list --name <substring> --limit <n>` when
@@ -106,8 +110,9 @@ guess a command name or parameter.
106
110
  never the basis of a write.
107
111
  4. `get` the complete target before mutating and work from the returned ids.
108
112
  5. Build the smallest valid patch. Nested objects merge recursively; plain arrays replace
109
- wholesale; dotted paths reach permitted leaves; deletion operators remove permitted keys. A
110
- `get` result contains read-only and derived fields, so build the patch from accepted fields only.
113
+ wholesale; dotted paths reach permitted leaves; `{"-=key": null}` deletes a permitted key, and
114
+ `{"==key": value}` replaces its whole value instead of merging. A `get` result contains
115
+ read-only and derived fields, so build the patch from accepted fields only.
111
116
  For arrays and extensible `system`/`flags` data: read, preserve everything that
112
117
  stays, change only the intended part.
113
118
  6. Dry-run every nontrivial mutation with the global `--dry-run` flag. It runs the same
@@ -171,6 +176,10 @@ Classify a failure before reacting:
171
176
  observed to change, not fully applied nested values. An embedded creation without a requested
172
177
  id may already exist despite an indeterminate error. Send only the remaining changes as a new
173
178
  operation with a fresh idempotency key, if using one.
179
+ - An update `INTERNAL_ERROR` with `details.paths` is a stored-vs-requested mismatch. Compare
180
+ `requested` and `stored` for each path. A representation-only difference, such as HTML escaping (`&` stored as `&amp;`) or
181
+ whitespace, can be accepted as done. Report any other difference to the user. The same patch
182
+ stores the same result, so the next request is either a different patch or none.
174
183
  - Correct a structured Foundry rejection and submit the corrected content as a new operation.
175
184
 
176
185
  ## Bulk writes and actions
@@ -1 +1 @@
1
- 248c52464e776236de6f937530e220dcb77b45a8b5cc754adbb4cfb4671a8c66
1
+ 5b4188ba39ed8f9f1c041cec9c5f0c3fb5d05db7910670d0ff2bb9c5d7d43dde