fvtt-world-cli 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -15
- package/bin/fvtt-world-cli.js +12 -0
- package/dist/index.js +2479 -128
- package/docs/README.md +2 -2
- package/docs/architecture.md +43 -11
- package/docs/commands.md +198 -37
- package/docs/compatibility.md +4 -2
- package/docs/getting-started.md +19 -10
- package/docs/protocol.md +114 -23
- package/docs/security.md +155 -22
- package/docs/skill.md +12 -8
- package/package.json +5 -6
- package/skills/foundry-world-editor/SKILL.md +68 -29
- package/skills/foundry-world-editor/SKILL.md.sha256 +1 -1
- package/bin/sync-installed-skill.mjs +0 -89
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: foundry-world-editor
|
|
3
|
-
description: Use
|
|
3
|
+
description: Use for live Foundry VTT reads or edits, or work with fvtt-world-cli, worldctl, the bridge, or daemon.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Foundry World Editor (fvtt-world-cli)
|
|
@@ -29,7 +29,7 @@ Authorization, name the browser in the `Browser label` field, and choose Pair; r
|
|
|
29
29
|
`fvtt-world-cli auth pending`, confirm the listed Origin, world, GM, browser label, and client id,
|
|
30
30
|
then `auth approve <code> --yes`. Never run bare `fvtt-world-cli auth`: that is the human's
|
|
31
31
|
one-command pairing wait, it blocks until a request arrives or Ctrl+C ends it, and it refuses
|
|
32
|
-
`--json
|
|
32
|
+
`--json`. Hand that command to the GM at their own terminal instead of running it here. A pairing
|
|
33
33
|
belongs to one browser, so the same GM on the same world can hold several records, and the label is
|
|
34
34
|
chosen in the browser and fixed once paired. Labels are not unique and can render as nothing, so the
|
|
35
35
|
client id is what tells concurrent requests apart and confirms that a re-pair replaces the record you
|
|
@@ -41,30 +41,58 @@ expect.
|
|
|
41
41
|
`BRIDGE_NOT_READY` means the daemon is reachable but no GM client is connected yet. `BRIDGE_BUSY`
|
|
42
42
|
means another paired browser holds the active bridge slot: clear it with
|
|
43
43
|
`fvtt-world-cli bridge release` and have the GM choose Connect in the module's Authorization
|
|
44
|
-
window or World CLI scene-controls group
|
|
44
|
+
window or World CLI scene-controls group. Keep the existing pairing.
|
|
45
|
+
|
|
46
|
+
`UNSUPPORTED_PROTOCOL_VERSION` means the installed components come from different releases. Read
|
|
47
|
+
`details.staleComponent`. Update the Foundry module for `module`. Update the CLI and restart the
|
|
48
|
+
daemon for `cli-daemon`. Compare the reported versions for `unknown`.
|
|
45
49
|
|
|
46
50
|
## Hard rules
|
|
47
51
|
|
|
48
52
|
- Pass `--json` on every automated call.
|
|
49
|
-
-
|
|
50
|
-
the control surface.
|
|
53
|
+
- Keep all world edits in the CLI. Leave Foundry world files and the browser UI untouched.
|
|
51
54
|
- Address documents by `id`, never by name; names are not unique. Embedded ids are meaningful only
|
|
52
55
|
with their complete parent chain (actor → item → effect, scene → token → item).
|
|
53
56
|
- Ownership changes go through dedicated `*.ownership.set` commands; raw `ownership` is rejected
|
|
54
57
|
in ordinary payloads.
|
|
55
|
-
- The CLI cannot
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
- The CLI cannot evaluate arbitrary JavaScript, edit compendium packs in place, or reach outside
|
|
59
|
+
the managed file boundary, and ordinary region-behavior writes reject executable types. Do not
|
|
60
|
+
look for workarounds; report the limitation instead.
|
|
61
|
+
- Check stderr before diagnosing a hung command. An approval-listed command prints a waiting line
|
|
62
|
+
while the GM decides in Foundry. Deletions require approval by default.
|
|
63
|
+
|
|
64
|
+
## Disabled commands
|
|
65
|
+
|
|
66
|
+
Some commands ship denied. A command that `schema <command>` or `--help` knows but a
|
|
67
|
+
`commands --json` listing with `policy.applied: true` omits is disabled by the GM of this bridge:
|
|
68
|
+
report that it needs enabling in the Command permissions window instead of hunting for an
|
|
69
|
+
equivalent. `macro.execute` is the only way to run code and is not a workaround for a missing
|
|
70
|
+
command.
|
|
71
|
+
|
|
72
|
+
## Command-specific cautions
|
|
73
|
+
|
|
74
|
+
A macro that throws fails `macro.execute` with a partial outcome — read what it touched before
|
|
75
|
+
retrying — while a macro that catches its own errors still returns `null`, which proves nothing;
|
|
76
|
+
verify effects with reads, and treat `MACRO_TIMEOUT` as indeterminate — the macro may still be
|
|
77
|
+
running.
|
|
78
|
+
`setting.set` cannot touch this module's own namespace (`SETTING_PROTECTED` is final; never retry).
|
|
79
|
+
When a setting write returns `requiresReload: true`, the change needs a GM-client reload to take
|
|
80
|
+
effect; `system.reload` performs it, drops the bridge, and requires a reconnect wait before the
|
|
81
|
+
next command. The reload commonly races its own result: the page can go away before the confirming
|
|
82
|
+
`reloading: true` is delivered, so a disconnect or `APPROVAL_UNKNOWN` there is the expected success
|
|
83
|
+
signal, not a failure — reconnect and continue rather than retrying the reload.
|
|
58
84
|
|
|
59
85
|
## The working loop
|
|
60
86
|
|
|
61
87
|
Discover → inspect schema → locate → read → smallest patch → dry-run → commit → verify. Never
|
|
62
88
|
guess a command name or parameter.
|
|
63
89
|
|
|
64
|
-
1. `fvtt-world-cli commands --json
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
90
|
+
1. Run `fvtt-world-cli commands --json`. Continue only when `policy.applied` is `true` and the
|
|
91
|
+
required command is present. A `false` value means no bridge answered and the output is the static
|
|
92
|
+
registry, not the client's permissions. Report an absent command instead of seeking another way
|
|
93
|
+
to produce the same effect. Warn the user before a command marked `"approval": true`, and prefer
|
|
94
|
+
one `*-many` command when it can replace several approvals. Dotted protocol names map to spaced
|
|
95
|
+
CLI subcommands, such as `actor.item.update` to `actor item update`.
|
|
68
96
|
2. `fvtt-world-cli schema <command>` shows the exact request parameters, required fields, enums,
|
|
69
97
|
and whether unknown fields are accepted; `fvtt-world-cli <command path> --help` maps flags.
|
|
70
98
|
3. Locate targets with the narrowest query: a family `list --name <substring> --limit <n>` when
|
|
@@ -73,16 +101,19 @@ guess a command name or parameter.
|
|
|
73
101
|
never the basis of a write.
|
|
74
102
|
4. `get` the complete target before mutating and work from the returned ids.
|
|
75
103
|
5. Build the smallest valid patch. Nested objects merge recursively; plain arrays replace
|
|
76
|
-
wholesale; dotted paths reach permitted leaves; deletion operators remove permitted keys.
|
|
77
|
-
|
|
78
|
-
|
|
104
|
+
wholesale; dotted paths reach permitted leaves; deletion operators remove permitted keys. A
|
|
105
|
+
`get` result contains read-only and derived fields, so build the patch from accepted fields only.
|
|
106
|
+
For arrays and extensible `system`/`flags` data: read, preserve everything that
|
|
79
107
|
stays, change only the intended part.
|
|
80
108
|
6. Dry-run every nontrivial mutation with the global `--dry-run` flag. It runs the same
|
|
81
109
|
validation, sanitization, permission, capability, and security checks, then stops before
|
|
82
110
|
persistence; check both `ok` and `.result.dryRun`. A preview reports only what is knowable
|
|
83
|
-
before execution and reserves nothing.
|
|
111
|
+
before execution and reserves nothing. Approval does not hold a preview. A denied command still
|
|
112
|
+
fails, and `approvalRequired: true` means the commit will wait for the GM.
|
|
84
113
|
7. Commit with the same content. Attach `--idempotency-key <stable-key>` to any create, clone,
|
|
85
|
-
import, upload, or action that might be retried
|
|
114
|
+
import, upload, or action that might be retried. Use one key per logical operation. After response
|
|
115
|
+
loss, follow the retry classification below because the delivery state determines whether to
|
|
116
|
+
reuse the key or create a fresh one.
|
|
86
117
|
8. Verify with a fresh read. This matters most after open-schema writes, bulk operations, and
|
|
87
118
|
actions, where confirmation can say less than an observed post-state.
|
|
88
119
|
|
|
@@ -103,22 +134,30 @@ details}}`, alongside `protocolVersion`, `type`, and the correlation `id` (echoi
|
|
|
103
134
|
`exec --stdin` batches). Branch on `error.code`, never on message text.
|
|
104
135
|
Documents live under a type-named key inside `result` that varies between commands but is stable
|
|
105
136
|
for each one. `id` is the public identifier; a `_id` mirror may accompany it. Values requested
|
|
106
|
-
through `include` flags may be derived or version-dependent
|
|
137
|
+
through `include` flags may be derived or version-dependent. Write them back only when the
|
|
107
138
|
update schema explicitly accepts them.
|
|
108
139
|
|
|
109
140
|
## Retry classification
|
|
110
141
|
|
|
111
142
|
Classify a failure before reacting:
|
|
112
143
|
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
the same call
|
|
116
|
-
-
|
|
117
|
-
|
|
118
|
-
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
144
|
+
- Fix usage and validation errors before sending a new request.
|
|
145
|
+
- Treat permission, safety, and capability errors as unavailable operations. Change the operation or
|
|
146
|
+
runtime instead of retrying the same call.
|
|
147
|
+
- Connection refusal, `BRIDGE_NOT_READY`, and `IDEMPOTENCY_STORE_FULL` mean Foundry received nothing.
|
|
148
|
+
Retry after restoring the connection or after earlier keys settle. Reuse the same request and key.
|
|
149
|
+
- `BRIDGE_TIMEOUT` and a response timeout after send are unresolved deliveries. Read world state
|
|
150
|
+
first. Reuse the same key only while the bridge session that carried the request remains connected.
|
|
151
|
+
- `BRIDGE_DISCONNECTED` is indeterminate. Read world state, report the result, and use a fresh key if
|
|
152
|
+
the operation still needs to run.
|
|
153
|
+
- `COMMAND_DENIED` means nothing ran and the command is unavailable in that client. Report the
|
|
154
|
+
limitation instead of seeking another command with the same effect.
|
|
155
|
+
- `APPROVAL_DENIED`, `APPROVAL_TIMEOUT`, and `APPROVAL_CANCELLED` mean nothing ran. Report the outcome
|
|
156
|
+
and wait for user direction. `APPROVAL_QUEUE_FULL` also means nothing ran, but it can clear after
|
|
157
|
+
earlier requests settle.
|
|
158
|
+
- `APPROVAL_UNKNOWN` and unconfirmed cancellation are indeterminate. Read the affected documents,
|
|
159
|
+
report the result, and use a fresh key if the operation still needs to run.
|
|
160
|
+
- Correct a structured Foundry rejection and submit the corrected content as a new operation.
|
|
122
161
|
|
|
123
162
|
## Bulk writes and actions
|
|
124
163
|
|
|
@@ -152,5 +191,5 @@ binary file into context.
|
|
|
152
191
|
`fvtt-world-cli docs` lists the durable contract documents shipped with the CLI, and
|
|
153
192
|
`fvtt-world-cli docs <name>` prints one: `commands` (human overview and workflows), `protocol`
|
|
154
193
|
(delivery, error, and session semantics), `security` (trust boundary), `compatibility` (Foundry
|
|
155
|
-
version differences), `getting-started` (first-run pairing walkthrough). The runtime registry
|
|
156
|
-
`commands --json` and `schema`
|
|
194
|
+
version differences), `getting-started` (first-run pairing walkthrough). The runtime registry in
|
|
195
|
+
`commands --json` and `schema` always outranks prose.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1834777e1d540cb4cc11b6eac862ff99878a6e4920ebbc1e2599871893e19dc9
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { cpSync, lstatSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
3
|
-
import { homedir, platform } from "node:os";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
|
|
7
|
-
const SKILL_NAME = "foundry-world-editor";
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
const binDirectory = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const sourceDirectory = [resolve(binDirectory, "../skills"), resolve(binDirectory, "../../../skills")]
|
|
12
|
-
.map((candidate) => resolve(candidate, SKILL_NAME))
|
|
13
|
-
.find((candidate) => {
|
|
14
|
-
try {
|
|
15
|
-
return statSync(join(candidate, "SKILL.md")).isFile();
|
|
16
|
-
} catch {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
if (!sourceDirectory) process.exit(0);
|
|
21
|
-
const packaged = readFileSync(join(sourceDirectory, "SKILL.md"), "utf8");
|
|
22
|
-
|
|
23
|
-
const home = process.env.HOME?.trim() || homedir();
|
|
24
|
-
const xdgHome = process.env.XDG_CONFIG_HOME?.trim();
|
|
25
|
-
const configPath = xdgHome
|
|
26
|
-
? join(xdgHome, "fvtt-world-cli", "config.json")
|
|
27
|
-
: platform() === "darwin"
|
|
28
|
-
? join(home, "Library", "Application Support", "fvtt-world-cli", "config.json")
|
|
29
|
-
: platform() === "win32"
|
|
30
|
-
? join(
|
|
31
|
-
process.env.APPDATA?.trim() || join(home, "AppData", "Roaming"),
|
|
32
|
-
"fvtt-world-cli",
|
|
33
|
-
"config.json"
|
|
34
|
-
)
|
|
35
|
-
: join(home, ".config", "fvtt-world-cli", "config.json");
|
|
36
|
-
|
|
37
|
-
const roots = new Set([join(home, ".agents", "skills")]);
|
|
38
|
-
try {
|
|
39
|
-
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
40
|
-
if (Array.isArray(config?.skillInstalls)) {
|
|
41
|
-
for (const root of config.skillInstalls) {
|
|
42
|
-
if (typeof root === "string" && root.length > 0) roots.add(root);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
} catch {
|
|
46
|
-
// no or unreadable config: the standard location is still checked
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
for (const root of roots) {
|
|
50
|
-
try {
|
|
51
|
-
const destination = join(root, SKILL_NAME);
|
|
52
|
-
const existing = (() => {
|
|
53
|
-
try {
|
|
54
|
-
return lstatSync(destination);
|
|
55
|
-
} catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
})();
|
|
59
|
-
if (!existing || existing.isSymbolicLink()) continue;
|
|
60
|
-
|
|
61
|
-
const installed = readFileSync(join(destination, "SKILL.md"), "utf8");
|
|
62
|
-
if (installed === packaged) continue;
|
|
63
|
-
|
|
64
|
-
const recorded = (() => {
|
|
65
|
-
try {
|
|
66
|
-
return readFileSync(join(destination, "SKILL.md.sha256"), "utf8").trim();
|
|
67
|
-
} catch {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
})();
|
|
71
|
-
|
|
72
|
-
if (recorded === createHash("sha256").update(installed).digest("hex")) {
|
|
73
|
-
rmSync(destination, { recursive: true, force: true });
|
|
74
|
-
cpSync(sourceDirectory, destination, { recursive: true });
|
|
75
|
-
console.error(
|
|
76
|
-
`fvtt-world-cli: updated the installed agent skill at ${destination} to the packaged version.`
|
|
77
|
-
);
|
|
78
|
-
} else {
|
|
79
|
-
console.error(
|
|
80
|
-
`fvtt-world-cli: the installed agent skill at ${destination} has local modifications and was NOT updated. Compare them, then run \`fvtt-world-cli skill update --force\` to replace it.`
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
} catch {
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
} catch {
|
|
88
|
-
process.exit(0);
|
|
89
|
-
}
|