fvtt-world-cli 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.1";
13
+ var PROTOCOL_VERSION = "1.3.0";
14
14
  var PROTOCOL_COMPONENTS = Object.freeze({
15
15
  MODULE: "module",
16
16
  CLI_DAEMON: "cli-daemon",
@@ -76,6 +76,9 @@ function resolveEffectiveLimits(uploadLimitBytes = DEFAULT_UPLOAD_SIZE_LIMIT_BYT
76
76
  wsMaxPayloadBytes: Math.max(wsMaxPayloadForUploadLimit(uploadLimitBytes), DEFAULT_WS_MAX_PAYLOAD_BYTES)
77
77
  };
78
78
  }
79
+ var BROWSER_STRING_MAX_LENGTH = 536870888;
80
+ var READ_RESPONSE_ENVELOPE_RESERVE_CHARS = 1024 * 1024;
81
+ var READ_ENCODING_CEILING_BYTES = Math.floor((BROWSER_STRING_MAX_LENGTH - READ_RESPONSE_ENVELOPE_RESERVE_CHARS) / 4) * 3;
79
82
  var BATCH_GET_MAX_IDS = 100;
80
83
  var BATCH_WRITE_MAX_ITEMS = 100;
81
84
  var BATCH_WRITE_STATUSES = Object.freeze([
@@ -102,6 +105,7 @@ var SCENE_THUMBNAIL_MAX_BYTES = 2 * 1024 * 1024;
102
105
  var SCENE_THUMBNAIL_RESPONSE_MAX_BYTES = 1024 * 1024;
103
106
  var TABLE_DRAW_MAX_COUNT = 100;
104
107
  var SETTING_VALUE_MAX_BYTES = 256 * 1024;
108
+ var SERIALIZE_OMISSIONS_MAX = 100;
105
109
  var MACRO_EXECUTE_TIMEOUT_DEFAULT_MS = 3e4;
106
110
  var MACRO_EXECUTE_TIMEOUT_MAX_MS = 3e5;
107
111
  var SEARCH_MODES = Object.freeze(["name", "full"]);
@@ -189,6 +193,7 @@ var ERROR_CODES = Object.freeze({
189
193
  PLAYLIST_NOT_FOUND: "PLAYLIST_NOT_FOUND",
190
194
  PLAYLIST_SOUND_NOT_FOUND: "PLAYLIST_SOUND_NOT_FOUND",
191
195
  JOURNAL_CATEGORY_NOT_FOUND: "JOURNAL_CATEGORY_NOT_FOUND",
196
+ JOURNAL_PAGE_NOT_FOUND: "JOURNAL_PAGE_NOT_FOUND",
192
197
  CHAT_MESSAGE_NOT_FOUND: "CHAT_MESSAGE_NOT_FOUND",
193
198
  TABLE_NOT_FOUND: "TABLE_NOT_FOUND",
194
199
  TABLE_RESULT_NOT_FOUND: "TABLE_RESULT_NOT_FOUND",
@@ -204,7 +209,6 @@ var ERROR_CODES = Object.freeze({
204
209
  FOLDER_NOT_FOUND: "FOLDER_NOT_FOUND",
205
210
  SETTING_NOT_FOUND: "SETTING_NOT_FOUND",
206
211
  SETTING_READ_FAILED: "SETTING_READ_FAILED",
207
- SETTING_VALUE_NOT_SERIALIZABLE: "SETTING_VALUE_NOT_SERIALIZABLE",
208
212
  SETTING_PROTECTED: "SETTING_PROTECTED",
209
213
  SETTING_UNREGISTERED: "SETTING_UNREGISTERED",
210
214
  MACRO_TIMEOUT: "MACRO_TIMEOUT",
@@ -815,6 +819,11 @@ var actorCommands = {
815
819
  },
816
820
  { mutation: true }
817
821
  ),
822
+ ...batchWriteCommands("actor.item", {
823
+ scopeProperties: actorScopeProperties,
824
+ createSchema: embeddedItemDataSchema,
825
+ patchSchema: itemPatchSchema
826
+ }),
818
827
  ...batchWriteCommands("actor", {
819
828
  scopeProperties: emptyScopeProperties,
820
829
  patchSchema: actorPatchSchema,
@@ -1810,6 +1819,22 @@ var auditFilesSchema = {
1810
1819
  },
1811
1820
  additionalProperties: false
1812
1821
  };
1822
+ var REFERENCE_SCOPES = deepFreeze([...AUDIT_FILE_SCOPES, "folder", "setting", "chat"]);
1823
+ var REFERENCE_DEFAULT_SCOPES = deepFreeze(REFERENCE_SCOPES.filter((scope) => scope !== "chat"));
1824
+ var worldReferencesSchema = {
1825
+ type: "object",
1826
+ required: ["uuid"],
1827
+ properties: {
1828
+ uuid: { type: "string", minLength: 1 },
1829
+ scope: {
1830
+ type: "array",
1831
+ minItems: 1,
1832
+ items: { type: "string", enum: [...REFERENCE_SCOPES] }
1833
+ },
1834
+ ...paginationProperties
1835
+ },
1836
+ additionalProperties: false
1837
+ };
1813
1838
  var fileCommands = {
1814
1839
  "file.list": cmd({
1815
1840
  type: "object",
@@ -1897,7 +1922,8 @@ var fileCommands = {
1897
1922
  )
1898
1923
  };
1899
1924
  var worldAuditCommands = {
1900
- "world.audit-files": cmd(auditFilesSchema)
1925
+ "world.audit-files": cmd(auditFilesSchema),
1926
+ "world.references": cmd(worldReferencesSchema)
1901
1927
  };
1902
1928
 
1903
1929
  // ../protocol/src/schemas/folder.js
@@ -2253,6 +2279,16 @@ var journalPageCreateSchema = {
2253
2279
  var journalPagePatchSchema = patchFrom(journalPageCreateSchema, {
2254
2280
  prepend: { id: { type: "string", minLength: 1 } }
2255
2281
  });
2282
+ var journalPageOwnPatchSchema = patchFrom(journalPageCreateSchema);
2283
+ var journalPageIdSchema = {
2284
+ type: "object",
2285
+ required: ["journalId", "pageId"],
2286
+ properties: {
2287
+ journalId: { type: "string", minLength: 1 },
2288
+ pageId: { type: "string", minLength: 1 }
2289
+ },
2290
+ additionalProperties: false
2291
+ };
2256
2292
  var journalDataSchema = {
2257
2293
  type: "object",
2258
2294
  required: ["name"],
@@ -2420,6 +2456,58 @@ var journalCommands = {
2420
2456
  patchSchema: journalDocumentPatchSchema,
2421
2457
  verbs: ["update-many", "delete-many"]
2422
2458
  }),
2459
+ "journal.page.list": cmd({
2460
+ type: "object",
2461
+ required: ["journalId"],
2462
+ properties: {
2463
+ journalId: { type: "string", minLength: 1 },
2464
+ ...nameFilterProperty,
2465
+ ...paginationProperties
2466
+ },
2467
+ additionalProperties: false
2468
+ }),
2469
+ "journal.page.get": cmd(journalPageIdSchema),
2470
+ "journal.page.create": cmd(
2471
+ {
2472
+ type: "object",
2473
+ required: ["journalId", "data"],
2474
+ properties: {
2475
+ journalId: { type: "string", minLength: 1 },
2476
+ data: journalPageCreateSchema,
2477
+ ...dryRunProperty,
2478
+ ...idempotencyKeyProperty
2479
+ },
2480
+ additionalProperties: false
2481
+ },
2482
+ { mutation: true }
2483
+ ),
2484
+ "journal.page.update": cmd(
2485
+ {
2486
+ type: "object",
2487
+ required: ["journalId", "pageId", "patch"],
2488
+ properties: {
2489
+ journalId: { type: "string", minLength: 1 },
2490
+ pageId: { type: "string", minLength: 1 },
2491
+ patch: journalPageOwnPatchSchema,
2492
+ ...dryRunProperty
2493
+ },
2494
+ additionalProperties: false
2495
+ },
2496
+ { mutation: true }
2497
+ ),
2498
+ "journal.page.delete": cmd(
2499
+ {
2500
+ type: "object",
2501
+ required: ["journalId", "pageId"],
2502
+ properties: {
2503
+ journalId: { type: "string", minLength: 1 },
2504
+ pageId: { type: "string", minLength: 1 },
2505
+ ...dryRunProperty
2506
+ },
2507
+ additionalProperties: false
2508
+ },
2509
+ { mutation: true }
2510
+ ),
2423
2511
  "journal.category.list": cmd({
2424
2512
  type: "object",
2425
2513
  required: ["journalId"],
@@ -5534,6 +5622,11 @@ var DEFAULT_COMMAND_PROFILE = deepFreeze({
5534
5622
  "journal.ownership.set": "allow",
5535
5623
  "journal.update-many": "allow",
5536
5624
  "journal.delete-many": "approve",
5625
+ "journal.page.list": "allow",
5626
+ "journal.page.get": "allow",
5627
+ "journal.page.create": "allow",
5628
+ "journal.page.update": "allow",
5629
+ "journal.page.delete": "approve",
5537
5630
  "journal.category.list": "allow",
5538
5631
  "journal.category.get": "allow",
5539
5632
  "journal.category.create": "allow",
@@ -5650,6 +5743,9 @@ var DEFAULT_COMMAND_PROFILE = deepFreeze({
5650
5743
  "actor.item.delete": "approve",
5651
5744
  "actor.item.clone": "allow",
5652
5745
  "actor.item.import-from-compendium": "allow",
5746
+ "actor.item.create-many": "allow",
5747
+ "actor.item.update-many": "allow",
5748
+ "actor.item.delete-many": "approve",
5653
5749
  "actor.update-many": "allow",
5654
5750
  "actor.delete-many": "approve",
5655
5751
  "actor.effect.list": "allow",
@@ -5703,6 +5799,7 @@ var DEFAULT_COMMAND_PROFILE = deepFreeze({
5703
5799
  "setting.set-many": "deny",
5704
5800
  "actor.import-from-compendium": "allow",
5705
5801
  "world.audit-files": "allow",
5802
+ "world.references": "allow",
5706
5803
  "world.search": "allow",
5707
5804
  "approval.await": "allow",
5708
5805
  "approval.cancel": "allow",
@@ -5781,6 +5878,17 @@ function editDistance(a, b) {
5781
5878
  }
5782
5879
  return previous[cols - 1];
5783
5880
  }
5881
+ function childPath(path, key) {
5882
+ return key.includes(".") ? `${path}[${JSON.stringify(key)}]` : `${path}.${key}`;
5883
+ }
5884
+ function describeDottedKey(key, properties) {
5885
+ const root = key.split(".")[0];
5886
+ if (!key.includes(".") || !Object.hasOwn(properties, root)) {
5887
+ return "";
5888
+ }
5889
+ const nested = key.split(".").reduceRight((inner, segment) => `{${JSON.stringify(segment)}: ${inner}}`, "...");
5890
+ return `; a key at this level must be one of the listed field names, so nest the rest of the path inside "${root}": ${nested}`;
5891
+ }
5784
5892
  function describeAllowedFields(key, allowedFields) {
5785
5893
  if (allowedFields.length === 0) {
5786
5894
  return "";
@@ -5830,7 +5938,7 @@ function validateObjectSchema(schema, value, path, errors) {
5830
5938
  const additionalProperties = schema.additionalProperties ?? true;
5831
5939
  for (const key of required) {
5832
5940
  if (!Object.hasOwn(value, key)) {
5833
- errors.push(`${path}.${key} is required`);
5941
+ errors.push(`${childPath(path, key)} is required`);
5834
5942
  }
5835
5943
  }
5836
5944
  const ownKeys = Object.keys(value);
@@ -5842,25 +5950,27 @@ function validateObjectSchema(schema, value, path, errors) {
5842
5950
  for (const key of ownKeys) {
5843
5951
  if (namePattern === null) {
5844
5952
  errors.push(
5845
- `${path}.${key} cannot be validated: ${schema.propertyNames.pattern} is not a valid unicode-mode pattern`
5953
+ `${childPath(path, key)} cannot be validated: ${schema.propertyNames.pattern} is not a valid unicode-mode pattern`
5846
5954
  );
5847
5955
  } else if (!namePattern.test(key)) {
5848
- errors.push(`${path}.${key} is not an allowed property name`);
5956
+ errors.push(`${childPath(path, key)} is not an allowed property name`);
5849
5957
  }
5850
5958
  }
5851
5959
  }
5852
5960
  for (const [key, propertyValue] of Object.entries(value)) {
5853
5961
  const propertySchema = properties[key];
5854
5962
  if (propertySchema) {
5855
- validateSchema(propertySchema, propertyValue, `${path}.${key}`, errors);
5963
+ validateSchema(propertySchema, propertyValue, childPath(path, key), errors);
5856
5964
  continue;
5857
5965
  }
5858
5966
  if (additionalProperties === false) {
5859
- errors.push(`${path}.${key} is not allowed${describeAllowedFields(key, Object.keys(properties))}`);
5967
+ errors.push(
5968
+ `${childPath(path, key)} is not allowed${describeAllowedFields(key, Object.keys(properties))}` + describeDottedKey(key, properties)
5969
+ );
5860
5970
  continue;
5861
5971
  }
5862
5972
  if (isPlainObject(additionalProperties)) {
5863
- validateSchema(additionalProperties, propertyValue, `${path}.${key}`, errors);
5973
+ validateSchema(additionalProperties, propertyValue, childPath(path, key), errors);
5864
5974
  }
5865
5975
  }
5866
5976
  }
@@ -9027,7 +9137,10 @@ function createSceneCreateParams(options) {
9027
9137
  function createSceneCloneParams(options) {
9028
9138
  return {
9029
9139
  sceneId: options.sceneId,
9030
- ...optionalPatch(buildScenePatch(options))
9140
+ ...optionalPatch({
9141
+ ...buildScenePatch(options),
9142
+ ...optionalJsonObject(options.patchJson, "--patch-json") ?? {}
9143
+ })
9031
9144
  };
9032
9145
  }
9033
9146
  function createItemCloneParams(options) {
@@ -9036,7 +9149,8 @@ function createItemCloneParams(options) {
9036
9149
  ...stringField("img", options.img),
9037
9150
  ...folderField(options),
9038
9151
  ...numberField("sort", options.sort),
9039
- ...jsonObjectField("system", options.systemJson, "--system-json")
9152
+ ...jsonObjectField("system", options.systemJson, "--system-json"),
9153
+ ...optionalJsonObject(options.patchJson, "--patch-json") ?? {}
9040
9154
  };
9041
9155
  return {
9042
9156
  itemId: options.itemId,
@@ -9047,7 +9161,8 @@ function createJournalCloneParams(options) {
9047
9161
  const patch = {
9048
9162
  ...stringField("name", options.name),
9049
9163
  ...folderField(options),
9050
- ...numberField("sort", options.sort)
9164
+ ...numberField("sort", options.sort),
9165
+ ...optionalJsonObject(options.patchJson, "--patch-json") ?? {}
9051
9166
  };
9052
9167
  return {
9053
9168
  journalId: options.journalId,
@@ -9133,7 +9248,8 @@ function createMacroCloneParams(options) {
9133
9248
  ...stringField("command", options.command),
9134
9249
  ...stringField("img", options.img),
9135
9250
  ...folderField(options),
9136
- ...stringField("scope", options.scope)
9251
+ ...stringField("scope", options.scope),
9252
+ ...optionalJsonObject(options.patchJson, "--patch-json") ?? {}
9137
9253
  };
9138
9254
  return {
9139
9255
  macroId: options.macroId,
@@ -9256,6 +9372,34 @@ function createJournalCategoryUpdateParams(options) {
9256
9372
  })
9257
9373
  };
9258
9374
  }
9375
+ function buildJournalPageFields(options) {
9376
+ return {
9377
+ ...stringField("name", options.name),
9378
+ ...stringField("type", options.type),
9379
+ ...numberField("sort", options.sort),
9380
+ ...options.clearCategory ? { category: null } : stringField("category", options.category),
9381
+ ...stringField("src", options.src)
9382
+ };
9383
+ }
9384
+ function createJournalPageCreateParams(options) {
9385
+ return {
9386
+ journalId: options.journalId,
9387
+ data: {
9388
+ ...buildJournalPageFields(options),
9389
+ ...optionalJsonObject(options.dataJson, "--data-json") ?? {}
9390
+ }
9391
+ };
9392
+ }
9393
+ function createJournalPageUpdateParams(options) {
9394
+ return {
9395
+ journalId: options.journalId,
9396
+ pageId: options.pageId,
9397
+ patch: assertNonEmptyPatch({
9398
+ ...buildJournalPageFields(options),
9399
+ ...optionalJsonObject(options.patchJson, "--patch-json") ?? {}
9400
+ })
9401
+ };
9402
+ }
9259
9403
  function buildTableFields(options) {
9260
9404
  return {
9261
9405
  ...stringField("description", options.description),
@@ -9568,7 +9712,8 @@ function createActorItemCloneParams(options) {
9568
9712
  ...stringField("name", options.name),
9569
9713
  ...stringField("img", options.img),
9570
9714
  ...numberField("sort", options.sort),
9571
- ...jsonObjectField("system", options.systemJson, "--system-json")
9715
+ ...jsonObjectField("system", options.systemJson, "--system-json"),
9716
+ ...optionalJsonObject(options.patchJson, "--patch-json") ?? {}
9572
9717
  };
9573
9718
  return {
9574
9719
  actorId: options.actorId,
@@ -9647,7 +9792,10 @@ function createActorUpdateParams(options) {
9647
9792
  function createActorCloneParams(options) {
9648
9793
  return {
9649
9794
  actorId: options.actorId,
9650
- ...optionalPatch(buildActorPatch(options))
9795
+ ...optionalPatch({
9796
+ ...buildActorPatch(options),
9797
+ ...optionalJsonObject(options.patchJson, "--patch-json") ?? {}
9798
+ })
9651
9799
  };
9652
9800
  }
9653
9801
  function createActorImportParams(options) {
@@ -11142,55 +11290,7 @@ function renderJournalDetails(journal) {
11142
11290
  });
11143
11291
  }
11144
11292
  pages.forEach((page, index) => {
11145
- const pageNumber = index + 1;
11146
- lines.push(`page ${pageNumber}: ${page?.id ?? ""} ${page?.name ?? ""} ${page?.type ?? ""}`);
11147
- const type = page?.type;
11148
- if (type === "image") {
11149
- if (page?.src) {
11150
- lines.push(`page ${pageNumber} src: ${page.src}`);
11151
- }
11152
- const caption = page?.image?.caption;
11153
- if (typeof caption === "string" && caption.length > 0) {
11154
- const shown = caption.length > 60 ? `${caption.slice(0, 60)}\u2026` : caption;
11155
- lines.push(`page ${pageNumber} caption: ${shown}`);
11156
- } else {
11157
- lines.push(`page ${pageNumber} caption: (none)`);
11158
- }
11159
- } else if (type === "video") {
11160
- if (page?.src) {
11161
- lines.push(`page ${pageNumber} src: ${page.src}`);
11162
- }
11163
- } else if (type === "pdf") {
11164
- if (page?.src) {
11165
- lines.push(`page ${pageNumber} src: ${page.src}`);
11166
- }
11167
- } else {
11168
- const format = page?.text?.format;
11169
- if (format === 2) {
11170
- lines.push(`page ${pageNumber} format: markdown`);
11171
- } else if (format === 1) {
11172
- lines.push(`page ${pageNumber} format: html`);
11173
- }
11174
- if (page?.text?.content) {
11175
- lines.push(`page ${pageNumber} text: ${page.text.content}`);
11176
- }
11177
- if (page?.src) {
11178
- lines.push(`page ${pageNumber} src: ${page.src}`);
11179
- }
11180
- }
11181
- const title = page?.title;
11182
- if (title && (title.show === false || typeof title.level === "number" && title.level !== 1)) {
11183
- const bits = [];
11184
- if (title.show === false) bits.push("show=false");
11185
- if (typeof title.level === "number" && title.level !== 1) bits.push(`level=${title.level}`);
11186
- lines.push(`page ${pageNumber} title: ${bits.join(" ")}`);
11187
- }
11188
- if (page?.category) {
11189
- lines.push(`page ${pageNumber} category: ${page.category}`);
11190
- }
11191
- if (page && Object.hasOwn(page, "ownership")) {
11192
- lines.push(...renderOwnershipLines(page.ownership).map((line) => `page ${pageNumber} ${line}`));
11193
- }
11293
+ lines.push(...renderJournalPageLines(page, `page ${index + 1}`));
11194
11294
  });
11195
11295
  return lines.join("\n");
11196
11296
  }
@@ -11203,6 +11303,143 @@ function renderJournalCategoryDetails(journalId, category) {
11203
11303
  `sort: ${category?.sort ?? 0}`
11204
11304
  ].join("\n");
11205
11305
  }
11306
+ function renderJournalPageLines(page, label) {
11307
+ const lines = [];
11308
+ lines.push(`${label}: ${page?.id ?? ""} ${page?.name ?? ""} ${page?.type ?? ""}`);
11309
+ const type = page?.type;
11310
+ if (type === "image") {
11311
+ if (page?.src) {
11312
+ lines.push(`${label} src: ${page.src}`);
11313
+ }
11314
+ const caption = page?.image?.caption;
11315
+ if (typeof caption === "string" && caption.length > 0) {
11316
+ const shown = caption.length > 60 ? `${caption.slice(0, 60)}\u2026` : caption;
11317
+ lines.push(`${label} caption: ${shown}`);
11318
+ } else {
11319
+ lines.push(`${label} caption: (none)`);
11320
+ }
11321
+ } else if (type === "video") {
11322
+ if (page?.src) {
11323
+ lines.push(`${label} src: ${page.src}`);
11324
+ }
11325
+ } else if (type === "pdf") {
11326
+ if (page?.src) {
11327
+ lines.push(`${label} src: ${page.src}`);
11328
+ }
11329
+ } else {
11330
+ const format = page?.text?.format;
11331
+ if (format === 2) {
11332
+ lines.push(`${label} format: markdown`);
11333
+ } else if (format === 1) {
11334
+ lines.push(`${label} format: html`);
11335
+ }
11336
+ if (page?.text?.content) {
11337
+ lines.push(`${label} text: ${page.text.content}`);
11338
+ }
11339
+ if (page?.src) {
11340
+ lines.push(`${label} src: ${page.src}`);
11341
+ }
11342
+ }
11343
+ const title = page?.title;
11344
+ if (title && (title.show === false || typeof title.level === "number" && title.level !== 1)) {
11345
+ const bits = [];
11346
+ if (title.show === false) bits.push("show=false");
11347
+ if (typeof title.level === "number" && title.level !== 1) bits.push(`level=${title.level}`);
11348
+ lines.push(`${label} title: ${bits.join(" ")}`);
11349
+ }
11350
+ if (page?.category) {
11351
+ lines.push(`${label} category: ${page.category}`);
11352
+ }
11353
+ if (page && Object.hasOwn(page, "ownership")) {
11354
+ lines.push(...renderOwnershipLines(page.ownership).map((line) => `${label} ${line}`));
11355
+ }
11356
+ return lines;
11357
+ }
11358
+ function renderJournalPageDetails(journalId, page) {
11359
+ return [`journal: ${journalId}`, ...renderJournalPageLines(page, "page")].join("\n");
11360
+ }
11361
+
11362
+ // src/render/settings.ts
11363
+ function renderSettingSummaryLine(setting) {
11364
+ const label = setting?.nameLocalized && setting.nameLocalized !== setting.name ? `${setting.nameLocalized} [${setting.name}]` : setting?.nameLocalized ?? setting?.name ?? "(unnamed)";
11365
+ return [
11366
+ setting?.id ?? `${setting?.namespace ?? "?"}.${setting?.key ?? "?"}`,
11367
+ label,
11368
+ `scope=${setting?.scope ?? "?"}`,
11369
+ `type=${setting?.type?.kind ?? "-"}`,
11370
+ `config=${Boolean(setting?.config)}`,
11371
+ `reload=${Boolean(setting?.requiresReload)}`,
11372
+ ...setting?.metadataReadFailed ? ["metadata=UNREADABLE"] : [],
11373
+ ...setting?.unaddressable ? ["address=UNADDRESSABLE"] : [],
11374
+ ...setting?.valueRedacted ? ["value=REDACTED"] : []
11375
+ ].join(" ");
11376
+ }
11377
+ function renderSettingValue(value) {
11378
+ return JSON.stringify(value ?? null, null, 2);
11379
+ }
11380
+ function renderOmissionLines(label, omissions, truncated) {
11381
+ if (!Array.isArray(omissions) || omissions.length === 0) return [];
11382
+ return [
11383
+ `${label} incomplete: ${omissions.length}${truncated ? "+" : ""} node(s) replaced by {"$unserializable": <reason>}`,
11384
+ ...omissions.map((omission) => ` ${omission?.path ?? "?"} ${omission?.reason ?? "?"}`)
11385
+ ];
11386
+ }
11387
+ function renderSettingWriteLines(row, dryRun) {
11388
+ const verb = dryRun ? "would write" : row?.changed ? "wrote" : "left unchanged";
11389
+ return [
11390
+ `${verb}: ${row?.id ?? `${row?.namespace}.${row?.key}`} (scope: ${row?.scope ?? "?"})`,
11391
+ `previous: ${renderSettingValue(row?.previous)}`,
11392
+ ...renderOmissionLines("previous", row?.previousOmissions, row?.previousOmissionsTruncated),
11393
+ `${dryRun ? "requested" : "value"}: ${renderSettingValue(row?.value)}`,
11394
+ ...renderOmissionLines("value", row?.valueOmissions, row?.valueOmissionsTruncated),
11395
+ `changed: ${Boolean(row?.changed)} / validated: ${Boolean(row?.validated)}`,
11396
+ ...row?.requiresReload ? ["requiresReload: true \u2014 Foundry acts on this only after `system reload` or a manual browser reload"] : ["requiresReload: false"]
11397
+ ];
11398
+ }
11399
+ function renderSettingWriteResult(result) {
11400
+ return renderSettingWriteLines(result, Boolean(result?.dryRun)).join("\n");
11401
+ }
11402
+ function renderSettingWriteOutcomes(result) {
11403
+ const outcomes = result?.outcomes ?? [];
11404
+ const dryRun = Boolean(result?.dryRun);
11405
+ const lines = [
11406
+ `${dryRun ? "Would write" : "Wrote"} settings (${outcomes.length}) \u2014 complete: ${Boolean(result?.complete)}`,
11407
+ ...outcomes.flatMap(
11408
+ (outcome) => outcome?.status === "updated" || outcome?.status === "unchanged" ? [
11409
+ `[${outcome.index}] ${outcome.status}`,
11410
+ ...renderSettingWriteLines(outcome, dryRun).map((line) => ` ${line}`)
11411
+ ] : [`[${outcome?.index}] ${outcome?.status} ${outcome?.id ?? ""}`]
11412
+ )
11413
+ ];
11414
+ if (result?.failure) {
11415
+ lines.push(`failure: ${result.failure.code} \u2014 ${result.failure.message}`);
11416
+ }
11417
+ return lines.join("\n");
11418
+ }
11419
+ function renderSettingBatchRows(setting) {
11420
+ return [
11421
+ renderSettingSummaryLine(setting),
11422
+ ...setting?.error ? [` error: ${setting.error.code} \u2014 ${setting.error.message}`] : [
11423
+ ` value: ${renderSettingValue(setting?.value)}`,
11424
+ ...renderOmissionLines("value", setting?.valueOmissions, setting?.valueOmissionsTruncated).map(
11425
+ (line) => ` ${line}`
11426
+ )
11427
+ ]
11428
+ ];
11429
+ }
11430
+ function renderSettingDetails(setting) {
11431
+ return [
11432
+ `setting: ${setting?.id ?? `${setting?.namespace}.${setting?.key}`}`,
11433
+ `name: ${setting?.nameLocalized ?? "(none)"}${setting?.name && setting.name !== setting.nameLocalized ? ` (raw: ${setting.name})` : ""}`,
11434
+ `hint: ${setting?.hintLocalized ?? "(none)"}${setting?.hint && setting.hint !== setting.hintLocalized ? ` (raw: ${setting.hint})` : ""}`,
11435
+ `scope: ${setting?.scope ?? "?"}`,
11436
+ `type: ${setting?.type?.kind ?? "(untyped)"}`,
11437
+ `config: ${Boolean(setting?.config)} / requiresReload: ${Boolean(setting?.requiresReload)}`,
11438
+ ...setting?.metadataReadFailed ? [`metadata: UNREADABLE (${setting.metadataReadError ?? "unknown error"})`] : [],
11439
+ setting?.valueRedacted ? "value: null (REDACTED \u2014 authorization settings are never exposed through setting commands)" : `value: ${JSON.stringify(setting?.value ?? null, null, 2)}`,
11440
+ ...renderOmissionLines("value", setting?.valueOmissions, setting?.valueOmissionsTruncated)
11441
+ ].join("\n");
11442
+ }
11206
11443
 
11207
11444
  // src/render/macros.ts
11208
11445
  var CHAT_CAPTURE_NOTES = {
@@ -11225,6 +11462,9 @@ function renderMacroExecuteResult(result) {
11225
11462
  `type: ${result?.type ?? "(unknown)"}`,
11226
11463
  `returned: ${JSON.stringify(result?.returned ?? null, null, 2)}`
11227
11464
  ];
11465
+ lines.push(
11466
+ ...renderOmissionLines("returned", result?.returnedOmissions, result?.returnedOmissionsTruncated)
11467
+ );
11228
11468
  if (result?.returnedOmitted) {
11229
11469
  lines.push(`returned omitted: ${result.returnedOmitted.code} \u2014 ${result.returnedOmitted.message}`);
11230
11470
  }
@@ -11578,73 +11818,6 @@ function renderRegionBehaviorDetails(sceneId, regionId, behavior) {
11578
11818
  ].join("\n");
11579
11819
  }
11580
11820
 
11581
- // src/render/settings.ts
11582
- function renderSettingSummaryLine(setting) {
11583
- const label = setting?.nameLocalized && setting.nameLocalized !== setting.name ? `${setting.nameLocalized} [${setting.name}]` : setting?.nameLocalized ?? setting?.name ?? "(unnamed)";
11584
- return [
11585
- setting?.id ?? `${setting?.namespace ?? "?"}.${setting?.key ?? "?"}`,
11586
- label,
11587
- `scope=${setting?.scope ?? "?"}`,
11588
- `type=${setting?.type?.kind ?? "-"}`,
11589
- `config=${Boolean(setting?.config)}`,
11590
- `reload=${Boolean(setting?.requiresReload)}`,
11591
- ...setting?.metadataReadFailed ? ["metadata=UNREADABLE"] : [],
11592
- ...setting?.unaddressable ? ["address=UNADDRESSABLE"] : [],
11593
- ...setting?.valueRedacted ? ["value=REDACTED"] : []
11594
- ].join(" ");
11595
- }
11596
- function renderSettingValue(value) {
11597
- return JSON.stringify(value ?? null, null, 2);
11598
- }
11599
- function renderSettingWriteLines(row, dryRun) {
11600
- const verb = dryRun ? "would write" : row?.changed ? "wrote" : "left unchanged";
11601
- return [
11602
- `${verb}: ${row?.id ?? `${row?.namespace}.${row?.key}`} (scope: ${row?.scope ?? "?"})`,
11603
- `previous: ${renderSettingValue(row?.previous)}`,
11604
- `${dryRun ? "requested" : "value"}: ${renderSettingValue(row?.value)}`,
11605
- `changed: ${Boolean(row?.changed)} / validated: ${Boolean(row?.validated)}`,
11606
- ...row?.requiresReload ? ["requiresReload: true \u2014 Foundry acts on this only after `system reload` or a manual browser reload"] : ["requiresReload: false"]
11607
- ];
11608
- }
11609
- function renderSettingWriteResult(result) {
11610
- return renderSettingWriteLines(result, Boolean(result?.dryRun)).join("\n");
11611
- }
11612
- function renderSettingWriteOutcomes(result) {
11613
- const outcomes = result?.outcomes ?? [];
11614
- const dryRun = Boolean(result?.dryRun);
11615
- const lines = [
11616
- `${dryRun ? "Would write" : "Wrote"} settings (${outcomes.length}) \u2014 complete: ${Boolean(result?.complete)}`,
11617
- ...outcomes.flatMap(
11618
- (outcome) => outcome?.status === "updated" || outcome?.status === "unchanged" ? [
11619
- `[${outcome.index}] ${outcome.status}`,
11620
- ...renderSettingWriteLines(outcome, dryRun).map((line) => ` ${line}`)
11621
- ] : [`[${outcome?.index}] ${outcome?.status} ${outcome?.id ?? ""}`]
11622
- )
11623
- ];
11624
- if (result?.failure) {
11625
- lines.push(`failure: ${result.failure.code} \u2014 ${result.failure.message}`);
11626
- }
11627
- return lines.join("\n");
11628
- }
11629
- function renderSettingBatchRows(setting) {
11630
- return [
11631
- renderSettingSummaryLine(setting),
11632
- ...setting?.error ? [` error: ${setting.error.code} \u2014 ${setting.error.message}`] : [` value: ${renderSettingValue(setting?.value)}`]
11633
- ];
11634
- }
11635
- function renderSettingDetails(setting) {
11636
- return [
11637
- `setting: ${setting?.id ?? `${setting?.namespace}.${setting?.key}`}`,
11638
- `name: ${setting?.nameLocalized ?? "(none)"}${setting?.name && setting.name !== setting.nameLocalized ? ` (raw: ${setting.name})` : ""}`,
11639
- `hint: ${setting?.hintLocalized ?? "(none)"}${setting?.hint && setting.hint !== setting.hintLocalized ? ` (raw: ${setting.hint})` : ""}`,
11640
- `scope: ${setting?.scope ?? "?"}`,
11641
- `type: ${setting?.type?.kind ?? "(untyped)"}`,
11642
- `config: ${Boolean(setting?.config)} / requiresReload: ${Boolean(setting?.requiresReload)}`,
11643
- ...setting?.metadataReadFailed ? [`metadata: UNREADABLE (${setting.metadataReadError ?? "unknown error"})`] : [],
11644
- setting?.valueRedacted ? "value: null (REDACTED \u2014 authorization settings are never exposed through setting commands)" : `value: ${JSON.stringify(setting?.value ?? null, null, 2)}`
11645
- ].join("\n");
11646
- }
11647
-
11648
11821
  // src/render/tables.ts
11649
11822
  function renderTableResultLine(result, ownerColumns = []) {
11650
11823
  const range = Array.isArray(result?.range) ? result.range.join("-") : "";
@@ -11950,6 +12123,28 @@ var RENDERERS = registerRenderers([
11950
12123
  }
11951
12124
  return lines.join("\n");
11952
12125
  }),
12126
+ customRenderer("world.references", (result, offset) => {
12127
+ const references = result.references ?? [];
12128
+ const target = result.target ?? {};
12129
+ const lines = [
12130
+ `target: ${target.uuid ?? ""} ${humanCell(target.name)} exists=${target.exists === null ? "unknown" : String(target.exists)}`
12131
+ ];
12132
+ if (references.length) {
12133
+ lines.push(
12134
+ `References (${references.length})`,
12135
+ ...references.map(
12136
+ (ref) => `${ref.uuid} ${humanCell(ref.name)} ${ref.field} ${ref.match}${ref.matchedUuid ? ` ${ref.matchedUuid}` : ""}`
12137
+ )
12138
+ );
12139
+ } else {
12140
+ lines.push("No stored references found.");
12141
+ }
12142
+ lines.push(...listFooterLines(result, references.length, offset));
12143
+ lines.push(
12144
+ `summary: references=${result.total ?? references.length} scannedDocuments=${result.scannedDocuments ?? 0} selfReferences=${result.selfReferences ?? 0} scopes=${(result.scopes ?? []).join(",")}`
12145
+ );
12146
+ return lines.join("\n");
12147
+ }),
11953
12148
  listRenderer("scene.list", {
11954
12149
  key: "scenes",
11955
12150
  empty: "No scenes found.",
@@ -12314,6 +12509,21 @@ var RENDERERS = registerRenderers([
12314
12509
  }),
12315
12510
  deleteRenderer("journal.delete", "journal"),
12316
12511
  detailRenderer("journal.show", (result) => renderJournalShowResult(result)),
12512
+ listRenderer("journal.page.list", {
12513
+ key: "pages",
12514
+ empty: "No pages found.",
12515
+ heading: "Pages",
12516
+ prefix: (result) => [`journal: ${result.journalId}`],
12517
+ row: (page) => `${page.id} ${page.name ?? ""} ${page.type ?? ""} sort=${page.sort ?? 0}${page.textFormat === 2 ? " format=markdown" : page.textFormat === 1 ? " format=html" : ""}`
12518
+ }),
12519
+ detailRenderer(
12520
+ ["journal.page.get", "journal.page.create", "journal.page.update"],
12521
+ (result) => renderJournalPageDetails(result.journalId, result.page)
12522
+ ),
12523
+ detailRenderer(
12524
+ "journal.page.delete",
12525
+ (result) => result.deleted ? `Deleted journal page ${result.id} from journal ${result.journalId}` : `Would delete journal page ${result.id} from journal ${result.journalId}`
12526
+ ),
12317
12527
  listRenderer("journal.category.list", {
12318
12528
  key: "categories",
12319
12529
  empty: "No categories found.",
@@ -13454,6 +13664,28 @@ function addJournalCategoryFieldOptions(command, verb) {
13454
13664
  const ordered = verb === "create" ? [JOURNAL_CATEGORY_SORT_FIELD] : [JOURNAL_CATEGORY_NAME_FIELD, JOURNAL_CATEGORY_SORT_FIELD];
13455
13665
  return addFieldOptions(command, verb, ordered);
13456
13666
  }
13667
+ var JOURNAL_PAGE_FIELDS = [
13668
+ {
13669
+ flag: "--sort <sort>",
13670
+ help: { create: "Page sort value", update: "New page sort value" },
13671
+ parser: parseNumber
13672
+ },
13673
+ {
13674
+ flag: "--category <categoryId>",
13675
+ help: {
13676
+ create: "Category id from journal category list",
13677
+ update: "New category id from journal category list"
13678
+ }
13679
+ },
13680
+ { flag: "--clear-category", help: "Explicitly set category to null", conflicts: "category" },
13681
+ {
13682
+ flag: "--src <src>",
13683
+ help: { create: "Media path of an image, pdf or video page", update: "New media path" }
13684
+ }
13685
+ ];
13686
+ function addJournalPageFieldOptions(command, verb) {
13687
+ return addFieldOptions(command, verb, JOURNAL_PAGE_FIELDS);
13688
+ }
13457
13689
  var PLAYLIST_FIELDS = [
13458
13690
  {
13459
13691
  flag: "--description <description>",
@@ -14318,7 +14550,7 @@ function registerActor({ program, dependencies }) {
14318
14550
  addActorFieldOptions(
14319
14551
  addIdempotencyKeyOption(actor.command("clone")).requiredOption("--actor-id <actorId>", "Source actor id").option("--name <name>", "Name override for the clone"),
14320
14552
  "clone"
14321
- ).action(async function cloneActorCommand(options) {
14553
+ ).option("--patch-json <json>", "Override fields for the clone as a JSON object (merged last)").action(async function cloneActorCommand(options) {
14322
14554
  await executeRemoteCommand({
14323
14555
  commandName: "actor.clone",
14324
14556
  params: createActorCloneParams(options),
@@ -14343,6 +14575,12 @@ function registerActor({ program, dependencies }) {
14343
14575
  });
14344
14576
  });
14345
14577
  const actorItem = actor.command("item").description("Foundry actor embedded item commands");
14578
+ registerBatchWriteCommands(actorItem, {
14579
+ prefix: "actor.item",
14580
+ noun: "item",
14581
+ scope: "actor",
14582
+ dataHelp: "Item data as a JSON ARRAY of objects; each element takes what `actor item create --data-json` takes plus name and type"
14583
+ });
14346
14584
  addNameFilterOption(
14347
14585
  addPaginationOptions(actorItem.command("list").requiredOption("--actor-id <actorId>", "Actor id")).option(
14348
14586
  "--include <fields>",
@@ -14408,7 +14646,7 @@ function registerActor({ program, dependencies }) {
14408
14646
  addEmbeddedItemFieldOptions(
14409
14647
  addIdempotencyKeyOption(actorItem.command("clone")).requiredOption("--actor-id <actorId>", "Actor id").requiredOption("--item-id <itemId>", "Source item id").option("--name <name>", "Name override for the clone"),
14410
14648
  "clone"
14411
- ).action(async function cloneActorItem(options) {
14649
+ ).option("--patch-json <json>", "Override fields for the clone as a JSON object (merged last)").action(async function cloneActorItem(options) {
14412
14650
  await executeRemoteCommand({
14413
14651
  commandName: "actor.item.clone",
14414
14652
  params: createActorItemCloneParams(options),
@@ -15066,7 +15304,7 @@ function registerBridge({ program, dependencies }) {
15066
15304
  });
15067
15305
  bridge.command("serve").description("Start the local bridge daemon").option("--request-timeout-ms <ms>", "Response timeout for forwarded Foundry requests", parsePositiveInt).option(
15068
15306
  "--upload-limit-bytes <size>",
15069
- "Override the file.upload size limit as bytes or a size like 100MiB (max 512MiB)"
15307
+ "Override the file.upload and file.read size limit for this run, as bytes or a size like 100MiB (max 512MiB)"
15070
15308
  ).action(async function serveBridge(options) {
15071
15309
  const bridgeConfig = getBridgeDaemonConfig(this, options, dependencies);
15072
15310
  const flagUploadLimitBytes = typeof options.uploadLimitBytes === "string" ? parseUploadLimitBytes(options.uploadLimitBytes) : void 0;
@@ -15845,7 +16083,9 @@ function registerConfig({ program, dependencies }) {
15845
16083
  write(dependencies.stdout, `${renderConfigGetHumanOutput(configPath, displayConfig)}
15846
16084
  `);
15847
16085
  });
15848
- config.command("set-upload-limit").description("Persist the file.upload size limit (raw bytes, pre-base64) for the daemon and CLI").argument("<size>", "Upload size limit as bytes or a size like 100MiB (max 512MiB)").action(function setUploadLimit(size) {
16086
+ config.command("set-upload-limit").description(
16087
+ "Persist the file.upload and file.read size limit (raw bytes, pre-base64) for the daemon and CLI"
16088
+ ).argument("<size>", "Upload size limit as bytes or a size like 100MiB (max 512MiB)").action(function setUploadLimit(size) {
15849
16089
  const uploadLimitBytes = parseUploadLimitBytes(size);
15850
16090
  writeCliConfigUpdate(dependencies.configStore, { uploadLimitBytes });
15851
16091
  const payload = {
@@ -16116,8 +16356,13 @@ function registerFile({ program, dependencies }) {
16116
16356
  dependencies
16117
16357
  });
16118
16358
  });
16119
- file.command("read").requiredOption("--path <path>", "Data-relative file path").addOption(
16359
+ file.command("read").description(
16360
+ `Read a file's content; a file over the upload limit (system info: limits.uploadBytes) or about ${Math.floor(READ_ENCODING_CEILING_BYTES / (1024 * 1024))} MiB returns PAYLOAD_TOO_LARGE`
16361
+ ).requiredOption("--path <path>", "Data-relative file path").addOption(
16120
16362
  new Option7("--encoding <encoding>", "Read encoding").choices(["text", "base64"]).makeOptionMandatory()
16363
+ ).addHelpText(
16364
+ "after",
16365
+ "\nThe CLI's receive limit follows the persisted limit (config set-upload-limit), not bridge serve --upload-limit-bytes; a larger response closes this command's connection.\nText escapes control bytes in JSON; read a binary file as base64."
16121
16366
  ).action(async function readFileCommand(options) {
16122
16367
  await executeRemoteCommand({
16123
16368
  commandName: "file.read",
@@ -16392,7 +16637,7 @@ function registerItem({ program, dependencies }) {
16392
16637
  addItemFieldOptions(
16393
16638
  addIdempotencyKeyOption(item.command("clone")).requiredOption("--item-id <itemId>", "Source item id").option("--name <name>", "Name override for the clone"),
16394
16639
  "clone"
16395
- ).action(async function cloneItem(options) {
16640
+ ).option("--patch-json <json>", "Override fields for the clone as a JSON object (merged last)").action(async function cloneItem(options) {
16396
16641
  await executeRemoteCommand({
16397
16642
  commandName: "item.clone",
16398
16643
  params: createItemCloneParams(options),
@@ -16563,7 +16808,7 @@ function registerJournal({ program, dependencies }) {
16563
16808
  addJournalFieldOptions(
16564
16809
  addIdempotencyKeyOption(journal.command("clone")).requiredOption("--journal-id <journalId>", "Source journal id").option("--name <name>", "Name override for the clone"),
16565
16810
  "clone"
16566
- ).action(async function cloneJournal(options) {
16811
+ ).option("--patch-json <json>", "Override fields for the clone as a JSON object (merged last)").action(async function cloneJournal(options) {
16567
16812
  await executeRemoteCommand({
16568
16813
  commandName: "journal.clone",
16569
16814
  params: createJournalCloneParams(options),
@@ -16579,6 +16824,64 @@ function registerJournal({ program, dependencies }) {
16579
16824
  dependencies
16580
16825
  });
16581
16826
  });
16827
+ const journalPage = journal.command("page").description("Journal-embedded page commands (one page at a time; journal update batches several)");
16828
+ journalPage.addHelpText(
16829
+ "after",
16830
+ `
16831
+ Result key (--json): .result.page (get and writes) / .result.pages[] (list), with .result.journalId.
16832
+ list returns summaries without text, system or flags, with textFormat (1 HTML, 2 Markdown, null on media pages); get returns the full page.
16833
+ Text fields belong to text pages, src to image, pdf and video pages, image.caption to image pages (schema journal.page.create).
16834
+ Text content goes through --data-json/--patch-json, e.g. --patch-json '{"text":{"content":"<p>\u2026</p>"}}'.
16835
+ Page ownership: journal ownership set --journal-id <id> --page-id <pageId>.`
16836
+ );
16837
+ addNameFilterOption(
16838
+ addPaginationOptions(journalPage.command("list").requiredOption("--journal-id <journalId>", "Journal id"))
16839
+ ).action(async function listJournalPages(options) {
16840
+ await executeRemoteCommand({
16841
+ commandName: "journal.page.list",
16842
+ params: { journalId: options.journalId, ...nameFilterParams(options), ...paginationParams(options) },
16843
+ command: this,
16844
+ dependencies
16845
+ });
16846
+ });
16847
+ journalPage.command("get").requiredOption("--journal-id <journalId>", "Journal id").requiredOption("--page-id <pageId>", "Page id").action(async function getJournalPage(options) {
16848
+ await executeRemoteCommand({
16849
+ commandName: "journal.page.get",
16850
+ params: { journalId: options.journalId, pageId: options.pageId },
16851
+ command: this,
16852
+ dependencies
16853
+ });
16854
+ });
16855
+ addJournalPageFieldOptions(
16856
+ addIdempotencyKeyOption(journalPage.command("create")).requiredOption("--journal-id <journalId>", "Journal id").requiredOption("--name <name>", "Page name").requiredOption("--type <type>", "Page type (text, image, pdf, video, or a system page type)"),
16857
+ "create"
16858
+ ).option("--data-json <json>", "Full/extra page data as a JSON object (merged last)").action(async function createJournalPage(options) {
16859
+ await executeRemoteCommand({
16860
+ commandName: "journal.page.create",
16861
+ params: createJournalPageCreateParams(options),
16862
+ command: this,
16863
+ dependencies
16864
+ });
16865
+ });
16866
+ addJournalPageFieldOptions(
16867
+ journalPage.command("update").requiredOption("--journal-id <journalId>", "Journal id").requiredOption("--page-id <pageId>", "Page id").option("--name <name>", "New page name").option("--type <type>", "New page type"),
16868
+ "update"
16869
+ ).option("--patch-json <json>", "Full/extra page patch as a JSON object (merged last)").action(async function updateJournalPage(options) {
16870
+ await executeRemoteCommand({
16871
+ commandName: "journal.page.update",
16872
+ params: createJournalPageUpdateParams(options),
16873
+ command: this,
16874
+ dependencies
16875
+ });
16876
+ });
16877
+ journalPage.command("delete").requiredOption("--journal-id <journalId>", "Journal id").requiredOption("--page-id <pageId>", "Page id").action(async function deleteJournalPage(options) {
16878
+ await executeRemoteCommand({
16879
+ commandName: "journal.page.delete",
16880
+ params: { journalId: options.journalId, pageId: options.pageId },
16881
+ command: this,
16882
+ dependencies
16883
+ });
16884
+ });
16582
16885
  const journalCategory = journal.command("category").description("Journal-embedded category commands (labels pages are grouped under)");
16583
16886
  journalCategory.addHelpText(
16584
16887
  "after",
@@ -16736,7 +17039,7 @@ function registerMacro({ program, dependencies }) {
16736
17039
  addMacroFieldOptions(
16737
17040
  addIdempotencyKeyOption(macro.command("clone")).requiredOption("--macro-id <macroId>", "Source macro id").option("--name <name>", "Name override for the clone"),
16738
17041
  "clone"
16739
- ).action(async function cloneMacroCommand(options) {
17042
+ ).option("--patch-json <json>", "Override fields for the clone as a JSON object (merged last)").action(async function cloneMacroCommand(options) {
16740
17043
  const command = await resolveMacroCommandBody(options, dependencies);
16741
17044
  await executeRemoteCommand({
16742
17045
  commandName: "macro.clone",
@@ -16758,6 +17061,10 @@ function registerMacro({ program, dependencies }) {
16758
17061
  "--macro-timeout-ms <ms>",
16759
17062
  `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.`
16760
17063
  ).argParser(parsePositiveInt)
17064
+ ).addHelpText(
17065
+ "after",
17066
+ `
17067
+ A return value node with no JSON form (a function, an own accessor, a circular reference) comes back as {"$unserializable": "<reason>"} and is listed in .result.returnedOmissions (at most ${SERIALIZE_OMISSIONS_MAX} entries); a return value over the size limits is dropped and explained in .result.returnedOmitted.`
16761
17068
  ).action(async function executeMacroCommand(options) {
16762
17069
  await executeRemoteCommand({
16763
17070
  commandName: "macro.execute",
@@ -17043,7 +17350,7 @@ function registerScene({ program, dependencies }) {
17043
17350
  addSceneFieldOptions(
17044
17351
  addIdempotencyKeyOption(scene.command("clone")).requiredOption("--scene-id <sceneId>", "Source scene id").option("--name <name>", "Name override for the clone"),
17045
17352
  "clone"
17046
- ).action(async function cloneScene(options) {
17353
+ ).option("--patch-json <json>", "Override fields for the clone as a JSON object (merged last)").action(async function cloneScene(options) {
17047
17354
  await executeRemoteCommand({
17048
17355
  commandName: "scene.clone",
17049
17356
  params: createSceneCloneParams(options),
@@ -17121,6 +17428,9 @@ function registerScene({ program, dependencies }) {
17121
17428
  "--include <fields>",
17122
17429
  "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
17430
  parseTokenListIncludeFields
17431
+ ).addHelpText(
17432
+ "after",
17433
+ "\nThe tokens of one actor across all scenes: world references --uuid Actor.<actorId> --scope scene"
17124
17434
  ).action(async function listSceneTokens(options) {
17125
17435
  await executeRemoteCommand({
17126
17436
  commandName: "scene.token.list",
@@ -17164,7 +17474,11 @@ function registerScene({ program, dependencies }) {
17164
17474
  addTokenFieldOptions(
17165
17475
  sceneToken.command("update").requiredOption("--scene-id <sceneId>", "Scene id").requiredOption("--token-id <tokenId>", "Token id"),
17166
17476
  "update"
17167
- ).option("--patch-json <json>", "Full/extra token patch as a JSON object (merged last)").action(async function updateSceneToken(options) {
17477
+ ).option("--patch-json <json>", "Full/extra token patch as a JSON object (merged last)").addHelpText(
17478
+ "after",
17479
+ `
17480
+ A patch with "delta": {"items": []} keeps the token's item overrides. scene token restore-delta drops every token override; scene token item update/delete changes single items.`
17481
+ ).action(async function updateSceneToken(options) {
17168
17482
  await executeRemoteCommand({
17169
17483
  commandName: "scene.token.update",
17170
17484
  params: createSceneTokenUpdateParams(options),
@@ -17270,12 +17584,13 @@ function registerScene({ program, dependencies }) {
17270
17584
  addEmbeddedItemFieldOptions(
17271
17585
  addIdempotencyKeyOption(sceneTokenItem.command("clone")).requiredOption("--scene-id <sceneId>", "Scene id").requiredOption("--token-id <tokenId>", "Token id").requiredOption("--item-id <itemId>", "Source item id").option("--name <name>", "Name override for the clone"),
17272
17586
  "clone"
17273
- ).action(async function cloneSceneTokenItem(options) {
17587
+ ).option("--patch-json <json>", "Override fields for the clone as a JSON object (merged last)").action(async function cloneSceneTokenItem(options) {
17274
17588
  const patch = {
17275
17589
  ...stringField("name", options.name),
17276
17590
  ...stringField("img", options.img),
17277
17591
  ...numberField("sort", options.sort),
17278
- ...jsonObjectField("system", options.systemJson, "--system-json")
17592
+ ...jsonObjectField("system", options.systemJson, "--system-json"),
17593
+ ...optionalJsonObject(options.patchJson, "--patch-json") ?? {}
17279
17594
  };
17280
17595
  await executeRemoteCommand({
17281
17596
  commandName: "scene.token.item.clone",
@@ -18144,7 +18459,11 @@ function registerSetting({ program, dependencies }) {
18144
18459
  });
18145
18460
  }
18146
18461
  );
18147
- setting.command("get").requiredOption("--namespace <namespace>", 'Setting namespace (e.g. "core" or a module id)').requiredOption("--key <key>", "Setting key within the namespace").action(async function getSetting(options) {
18462
+ setting.command("get").requiredOption("--namespace <namespace>", 'Setting namespace (e.g. "core" or a module id)').requiredOption("--key <key>", "Setting key within the namespace").addHelpText(
18463
+ "after",
18464
+ `
18465
+ A value node with no JSON form (an accessor property, a function, a circular reference) comes back as {"$unserializable": "<reason>"} and is listed in .result.setting.valueOmissions (at most ${SERIALIZE_OMISSIONS_MAX} entries; valueOmissionsTruncated marks a longer list). Such a value is incomplete: writing it back with setting set stores the markers as data.`
18466
+ ).action(async function getSetting(options) {
18148
18467
  await executeRemoteCommand({
18149
18468
  commandName: "setting.get",
18150
18469
  params: { namespace: options.namespace, key: options.key },
@@ -18158,7 +18477,7 @@ function registerSetting({ program, dependencies }) {
18158
18477
  "--ids <list>",
18159
18478
  'Comma-separated setting ids in "namespace.key" form (e.g. core.chatBubbles,my-module.apiKey)',
18160
18479
  parseIdList
18161
- ).action(async function getManySettings(options) {
18480
+ ).addHelpText("after", "\nEach row reports omissions like setting get, in valueOmissions.").action(async function getManySettings(options) {
18162
18481
  await executeRemoteCommand({
18163
18482
  commandName: "setting.get-many",
18164
18483
  params: { ids: options.ids },
@@ -18170,6 +18489,9 @@ function registerSetting({ program, dependencies }) {
18170
18489
  "--value-json <json>",
18171
18490
  'New value as a JSON literal \u2014 true, 42, "text", [..] or {..}; a quoted string stays distinct from a boolean or a number',
18172
18491
  (value) => parseJsonValue(value, "--value-json")
18492
+ ).addHelpText(
18493
+ "after",
18494
+ '\nprevious and value report omissions like setting get, in previousOmissions and valueOmissions. {"$unserializable": ...} markers in --value-json are stored as data.'
18173
18495
  ).action(async function setSetting(options) {
18174
18496
  await executeRemoteCommand({
18175
18497
  commandName: "setting.set",
@@ -18730,6 +19052,7 @@ function registerWorld({ program, dependencies }) {
18730
19052
  "",
18731
19053
  "Result key (--json): .result.broken[] (+ .result.checkedRefs / .result.checkedFiles / .result.skipped[], total/hasMore over broken) for audit-files.",
18732
19054
  "Result key (--json): .result.results[] (+ .result.total / .result.hasMore / .result.index) for search.",
19055
+ "Result key (--json): .result.references[] (+ .result.target / .result.total / .result.hasMore / .result.scannedDocuments / .result.selfReferences / .result.scopes) for references.",
18733
19056
  "",
18734
19057
  "search: results are SECTIONED \u2014 world rows first, then pack rows, each ranked among itself.",
18735
19058
  "`score` is comparable only WITHIN a section (the two corpora are separate indexes), so compare",
@@ -18761,6 +19084,53 @@ function registerWorld({ program, dependencies }) {
18761
19084
  dependencies
18762
19085
  });
18763
19086
  });
19087
+ addPaginationOptions(
19088
+ world.command("references").description("Report where stored world data references a document, by its UUID").requiredOption(
19089
+ "--uuid <uuid>",
19090
+ "Target document UUID: Actor.<id>, Scene.<id>.Token.<id>, Compendium.<package>.<pack>.<Type>.<id>, \u2026"
19091
+ ).option(
19092
+ "--scope <list>",
19093
+ `Comma-separated document families to scan: ${REFERENCE_SCOPES.join(",")} (default: ${REFERENCE_DEFAULT_SCOPES.join(",")}; chat only on request)`,
19094
+ parseScopeList
19095
+ ).addHelpText(
19096
+ "after",
19097
+ [
19098
+ "",
19099
+ "Scans the stored data, flags and _stats included, of every world document in scope, its embedded",
19100
+ "documents, and every user. Each row names the owning document by UUID and the dotted field path in it;",
19101
+ "a match inside a token's delta is reported on the token (field delta.\u2026). An embedded target needs its",
19102
+ "full UUID (Scene.<s>.Token.<t>).",
19103
+ "match: uuid a field holds the target UUID",
19104
+ " link an @UUID[...] link or a legacy @Actor[<id>] / @Compendium[<pack>.<id>] link names the target",
19105
+ " embedded a field holds the UUID of a document embedded in the target (matchedUuid); deleting the",
19106
+ " target breaks it too",
19107
+ " key an object key names the target; a dotted key Foundry stored nested is reported at the",
19108
+ " target's id (flags.mod.Actor.<id>)",
19109
+ " relative a relative link (@UUID[.<id>], @Embed[.<id>]) in journal page text or a roll table or",
19110
+ " table result description resolves to the target or a document in it (matchedUuid)",
19111
+ " id a foreign-key field of the target's type (token actorId, note entryId, folder, setting user,",
19112
+ " chat speaker, \u2026) or a hotbar slot (hotbar.<slot>) holds the target's id; for an embedded",
19113
+ " target only where the record also names its parent or sits inside it, and never for a",
19114
+ " deeper target such as a token's actor",
19115
+ "setting: world and user settings, read as stored JSON (rows Setting.<id>, name = key, field value[.<path>]).",
19116
+ "Not found: compendium contents, client settings, bare ids outside foreign-key fields (system data, object",
19117
+ "keys, chat whisper lists) or in fields Foundry maintains itself (_stats.lastModifiedBy), name-based links",
19118
+ "(@Actor[Goblin]), relative links in other fields. References inside the target itself are counted in",
19119
+ "selfReferences. target.exists is null for a compendium target, whose pack is not loaded."
19120
+ ].join("\n")
19121
+ )
19122
+ ).action(async function findReferences(options) {
19123
+ await executeRemoteCommand({
19124
+ commandName: "world.references",
19125
+ params: {
19126
+ uuid: options.uuid,
19127
+ ...options.scope ? { scope: options.scope } : {},
19128
+ ...paginationParams(options)
19129
+ },
19130
+ command: this,
19131
+ dependencies
19132
+ });
19133
+ });
18764
19134
  addPaginationOptions(
18765
19135
  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(
18766
19136
  new Option15(
package/docs/commands.md CHANGED
@@ -28,13 +28,13 @@ Use these families to narrow your search, then check their help for supported op
28
28
  | Task | Command families |
29
29
  |---|---|
30
30
  | Edit world content | `actor`, `item`, `journal`, `scene`, `macro`, `playlist`, `table`, `cards` |
31
- | Edit embedded content | Nested families such as `actor.item`, `playlist.sound`, `table.result`, and `*.effect` |
31
+ | Edit embedded content | Nested families such as `actor.item`, `journal.page`, `playlist.sound`, `table.result`, and `*.effect` |
32
32
  | Edit scene placeables | `scene.token`, `scene.wall`, `scene.region`, and related families |
33
33
  | Edit scene levels (v14) | `scene.level` |
34
34
  | Manage encounters | `combat`, `combat.combatant`, `combat.group` |
35
35
  | Organize content and access | `folder`, `*.ownership.set`, `user` |
36
36
  | Manage chat and settings | `chat`, `setting` |
37
- | Find content or missing assets | `world.search`, `world.audit-files` |
37
+ | Find content, references, or missing assets | `world.search`, `world.references`, `world.audit-files` |
38
38
  | Read packs or import world documents | `compendium`, supported `*.import-from-compendium` commands |
39
39
  | Manage assets | `file` |
40
40
  | Control playback and gameplay | Actions within `playlist`, `table`, `cards`, `combat`, `scene`, and `game` |
@@ -277,7 +277,7 @@ For scripts and JSON output, pass `--yes` to skip the listing and prompt.
277
277
  ## Local configuration
278
278
 
279
279
  `config get` shows the config path and non-secret settings. `config set-upload-limit <size>`
280
- changes the raw upload-byte limit; restart the daemon for it to take effect.
280
+ changes the raw byte limit for uploads and file reads; it takes effect after a daemon restart.
281
281
 
282
282
  If set, `XDG_CONFIG_HOME` places configuration under `$XDG_CONFIG_HOME/fvtt-world-cli`. Otherwise:
283
283
 
@@ -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
+ | Relative content links | `@UUID[..<id>]` points at nothing | `@UUID[..<id>]` names a document one parent up | `world.references` reports a `..` link only on v14. |
14
15
  | 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. |
15
16
 
16
17
  Scene placeable fields and available region behavior types also differ. Read the target document
package/docs/protocol.md CHANGED
@@ -27,12 +27,13 @@ a correlated control-request error.
27
27
 
28
28
  ## Size limits
29
29
 
30
- The upload limit and the transport frame limit are distinct: the first bounds raw upload content,
31
- the second accounts for encoding and envelope overhead and is never lower than what legitimate
32
- large read responses require. The daemon advertises its effective limits during the handshake, and
33
- the bridge checks response size before sending, so an oversized response returns
34
- `PAYLOAD_TOO_LARGE` instead of destroying the shared session. A persisted upload-limit change is
35
- applied to the transport only after the daemon restarts.
30
+ The upload limit and the transport frame limit are distinct: the first bounds raw upload content
31
+ and the raw size of a `file.read`, the second accounts for encoding and envelope overhead and is
32
+ never lower than what legitimate large read responses require. The daemon advertises its effective
33
+ limits during the handshake, and the bridge checks response size before sending, so an oversized
34
+ response, or one the browser cannot serialize, returns `PAYLOAD_TOO_LARGE` instead of destroying the
35
+ shared session. A persisted upload-limit change is applied to the transport only after the daemon
36
+ restarts.
36
37
 
37
38
  ## Pairing
38
39
 
@@ -187,6 +188,11 @@ World document reads carry `compendiumSource` and `duplicateSource`, hoisted fro
187
188
  Broadcast actions report `dispatched` and target users rather than a confirmed player-side result.
188
189
  The active/inactive split is included where knowable.
189
190
 
191
+ A setting value or macro return value node with no JSON form, such as a function, an own accessor
192
+ property or a circular reference, is replaced by `{"$unserializable": "<reason>"}` and listed with
193
+ its path in an `...Omissions` field. A value with omissions is incomplete; writing it back stores the
194
+ markers as data.
195
+
190
196
  ### Macro results
191
197
 
192
198
  `macro.execute` returns the macro's value and observed chat messages. `chatCapture` describes the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fvtt-world-cli",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
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.1"
59
+ "@fvtt-world-cli/protocol": "1.3.0"
60
60
  }
61
61
  }
@@ -19,10 +19,11 @@ fvtt-world-cli system info --json
19
19
  ```
20
20
 
21
21
  Proceed only when `.result.bridge.status` is `connected`, and verify the reported world and GM are
22
- the intended targets. If the daemon is not running, start `fvtt-world-cli bridge serve` as a
23
- persistent background process (it does not daemonize itself) and leave it running for the whole
24
- session. If no bridge connects, a GM must have the target world open in a browser with the
25
- `fvtt-world-cli` module enabled and paired.
22
+ the intended targets. If `system info` answers at all, a daemon is running; use it. Start
23
+ `fvtt-world-cli bridge serve` only when the daemon is unreachable, as a persistent background
24
+ process (it does not daemonize itself), and leave it running for the whole session. If no bridge
25
+ connects, a GM must have the target world open in a browser with the `fvtt-world-cli` module
26
+ enabled and paired.
26
27
 
27
28
  Pairing is a first-run trust decision that needs the human: ask the GM to open Module Settings →
28
29
  Authorization, name the browser in the `Browser label` field, and choose Pair; run
@@ -81,8 +82,11 @@ Deleting a level also deletes its tokens and every placeable only on that level,
81
82
  does not list them: read the scene's placeables (token `level`, others' `levels`) and tell the user
82
83
  what the delete removes before running it.
83
84
  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.
85
+ editing an actor that has placed tokens, read its tokens' overrides
86
+ (`scene token get --include delta,prepared`) and tell the user which tokens the edit will not
87
+ change.
88
+ Before deleting or relinking a document, run `world references --uuid <uuid>` and tell the user
89
+ which stored references will point at nothing.
86
90
  `setting.set` cannot touch this module's own namespace (`SETTING_PROTECTED` is final; never retry).
87
91
  When a setting write returns `requiresReload: true`, the change needs a GM-client reload to take
88
92
  effect; `system.reload` performs it, drops the bridge, and requires a reconnect wait before the
@@ -1 +1 @@
1
- 5b4188ba39ed8f9f1c041cec9c5f0c3fb5d05db7910670d0ff2bb9c5d7d43dde
1
+ 07f46d03812a74d1643f7483eb3eb926244dd1837b7afb11280c3072bfa3d5bd