fvtt-world-cli 1.0.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/LICENSE +21 -0
- package/README.md +136 -0
- package/bin/fvtt-world-cli.js +47 -0
- package/bin/prepare-package-assets.mjs +22 -0
- package/bin/sync-installed-skill.mjs +89 -0
- package/dist/index.js +16200 -0
- package/docs/README.md +71 -0
- package/docs/architecture.md +216 -0
- package/docs/commands.md +367 -0
- package/docs/compatibility.md +26 -0
- package/docs/getting-started.md +104 -0
- package/docs/images/authorization-window.png +0 -0
- package/docs/images/icon.svg +16 -0
- package/docs/images/mark.svg +15 -0
- package/docs/images/module-settings.png +0 -0
- package/docs/protocol.md +238 -0
- package/docs/security.md +233 -0
- package/docs/skill.md +65 -0
- package/package.json +50 -0
- package/skills/foundry-world-editor/SKILL.md +156 -0
- package/skills/foundry-world-editor/SKILL.md.sha256 +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DroiD16
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# World CLI for Foundry VTT
|
|
2
|
+
|
|
3
|
+
[](https://foundryvtt.com)
|
|
4
|
+
|
|
5
|
+
Tell your AI agent what should change in your Foundry VTT world, and it happens — in the live
|
|
6
|
+
world, validated by Foundry, visible to your players immediately.
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
You → agent: “The scimitar in Valeros's inventory should burn targets on hit —
|
|
10
|
+
add a Flaming effect to it.”
|
|
11
|
+
|
|
12
|
+
agent → fvtt-world-cli:
|
|
13
|
+
actor list --name Valeros find the character
|
|
14
|
+
actor item list --name scimitar find the sword in their inventory
|
|
15
|
+
actor item effect create … add the effect to it
|
|
16
|
+
|
|
17
|
+
agent → you: “Done — Flaming effect added, already live in the world.”
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Modern agents already understand requests like that. What they have lacked is a safe way into
|
|
21
|
+
Foundry: driving the browser UI is brittle and token consuming, and editing world files on disk
|
|
22
|
+
bypasses everything Foundry does to keep a world consistent. fvtt-world-cli is the missing bridge —
|
|
23
|
+
a command line wired into your open GM session that performs every change through Foundry's own
|
|
24
|
+
APIs, exactly as if a GM had made it in the UI.
|
|
25
|
+
|
|
26
|
+
Two things it's not. It is not an AI game master: it does not run the game, it only edits the
|
|
27
|
+
world when asked. And it includes no AI of its own: you connect the agent you already use, such as
|
|
28
|
+
Codex, Claude Code, or Hermes.
|
|
29
|
+
|
|
30
|
+
## What your agent can do
|
|
31
|
+
|
|
32
|
+
The everyday GM work: build and edit characters and their inventories, write journals, manage
|
|
33
|
+
scenes down to individual tokens, walls, and lights, and pull content in from compendiums.
|
|
34
|
+
|
|
35
|
+
A few asks it handles end to end:
|
|
36
|
+
|
|
37
|
+
- “Sort the journals into folders by location.”
|
|
38
|
+
- “Dim every light in the tavern scene down to torchlight.”
|
|
39
|
+
- “Find every NPC that still has no portrait and list them.”
|
|
40
|
+
- “Turn the bestiary goblin into a flying one that throws dynamite for 2d6 damage, and add it to
|
|
41
|
+
the scene.”
|
|
42
|
+
|
|
43
|
+
[Commands](docs/commands.md) maps the full surface.
|
|
44
|
+
|
|
45
|
+
## Setting up
|
|
46
|
+
|
|
47
|
+
The simplest setup is one step: point your AI agent at this repository and ask it to set
|
|
48
|
+
fvtt-world-cli up. Then follow its lead — it will most likely ask you to install the module in
|
|
49
|
+
Foundry and click *Pair* there, and it handles the rest itself.
|
|
50
|
+
|
|
51
|
+
### Manual setup
|
|
52
|
+
|
|
53
|
+
Two pieces install separately and pair on first run: the CLI on your machine and a bridge module in
|
|
54
|
+
Foundry. The CLI connects to the module in your open GM session, and through that connection the
|
|
55
|
+
agent works with the world.
|
|
56
|
+
|
|
57
|
+
1. Install the CLI (Node.js 20+):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install -g fvtt-world-cli
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
2. Install the module (Foundry VTT v13–v14): paste the manifest URL into Foundry's
|
|
64
|
+
*Install Module* dialog, then enable the module in the target world:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
https://github.com/DroiD16/fvtt-world-cli/releases/latest/download/module.json
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
3. Start the daemon and leave it running:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
fvtt-world-cli bridge serve
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
4. Pair the browser: choose *Pair* in the module's Authorization window, reachable from the
|
|
77
|
+
*World CLI* group in the scene controls or from the module's settings. Then approve the request
|
|
78
|
+
from the terminal — it shows the requesting origin, world, GM, and browser, and asks for a yes
|
|
79
|
+
or no:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
fvtt-world-cli auth
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
5. Optionally, install the packaged skill into your AI agent — the next section explains what it
|
|
86
|
+
does:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
fvtt-world-cli skill install
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Pairing happens once per browser; afterwards the bridge reconnects on its own whenever the daemon
|
|
93
|
+
and a paired GM client are both up. [Getting started](docs/getting-started.md) walks through the
|
|
94
|
+
first run in detail. The canonical executable is `fvtt-world-cli`; `worldctl` is an equivalent
|
|
95
|
+
short alias.
|
|
96
|
+
|
|
97
|
+
## Handing it to your agent
|
|
98
|
+
|
|
99
|
+
The package ships `foundry-world-editor`, an installable [Agent Skill](docs/skill.md) that teaches
|
|
100
|
+
an agent to drive the CLI safely. It follows the open Agent Skills standard, so the same skill
|
|
101
|
+
works in Claude Code, Codex, and other agents that read `SKILL.md` files.
|
|
102
|
+
|
|
103
|
+
An installed skill stays current on its own: updating the package refreshes it. A copy you have
|
|
104
|
+
edited locally is never replaced automatically, so your own instructions survive updates.
|
|
105
|
+
|
|
106
|
+
## Built to be trusted with a live world
|
|
107
|
+
|
|
108
|
+
The whole design assumes an automated caller that must not be able to exceed its intended
|
|
109
|
+
authority; [Security](docs/security.md) covers the boundaries in full. In short:
|
|
110
|
+
|
|
111
|
+
- Everything stays on your machine: the daemon accepts loopback connections only, and there is no
|
|
112
|
+
internet-facing mode.
|
|
113
|
+
- Nothing connects without your approval: every browser pairs once through an explicit yes at your
|
|
114
|
+
terminal, and no secrets are printed along the way.
|
|
115
|
+
- Foundry remains the authority: every change runs through the same Document APIs and validation
|
|
116
|
+
the UI uses, under the GM's permissions.
|
|
117
|
+
- Any change can be previewed before it happens: a global `--dry-run` flag runs the same
|
|
118
|
+
validation and guards as a real call and stops before mutation.
|
|
119
|
+
- There is no arbitrary-code path: commands are typed and validated on both sides of the transport,
|
|
120
|
+
and executable content such as scripted region behaviors is blocked on every write route.
|
|
121
|
+
- File access is confined to the active world's managed assets and always excludes its manifest,
|
|
122
|
+
databases, and packs.
|
|
123
|
+
|
|
124
|
+
## Foundry compatibility
|
|
125
|
+
|
|
126
|
+
Supported on Foundry VTT v13 and v14 (both verified). Version-dependent behavior is
|
|
127
|
+
capability-gated and reported per command; [Compatibility](docs/compatibility.md) lists the
|
|
128
|
+
differences that matter to operators.
|
|
129
|
+
|
|
130
|
+
## Documentation
|
|
131
|
+
|
|
132
|
+
[docs/README.md](docs/README.md) maps the full set: commands, protocol, security, architecture,
|
|
133
|
+
compatibility, and the agent skill. The same documents ship inside the package and are printed by
|
|
134
|
+
`fvtt-world-cli docs [document]`, so the installed CLI is self-describing offline.
|
|
135
|
+
|
|
136
|
+
Bug reports and questions go to [GitHub Issues](https://github.com/DroiD16/fvtt-world-cli/issues).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
const binDirectory = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const compiledEntry = resolve(binDirectory, "../dist/index.js");
|
|
10
|
+
const sourceEntry = resolve(binDirectory, "../src/index.ts");
|
|
11
|
+
|
|
12
|
+
if (existsSync(compiledEntry) && process.env.FVTT_WORLD_CLI_FORCE_SRC !== "1") {
|
|
13
|
+
try {
|
|
14
|
+
const cli = await import(pathToFileURL(compiledEntry).href);
|
|
15
|
+
process.exitCode = await cli.executeCli(process.argv);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error("Failed to launch fvtt-world-cli", error);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
const [nodeMajor, nodeMinor] = process.versions.node.split(".").map(Number);
|
|
22
|
+
const supportsDisableWarning =
|
|
23
|
+
(nodeMajor === 20 && nodeMinor >= 11) || (nodeMajor === 21 && nodeMinor >= 3) || nodeMajor >= 22;
|
|
24
|
+
const nodeArgs = supportsDisableWarning
|
|
25
|
+
? ["--disable-warning=DEP0205", "--import", "tsx/esm", sourceEntry]
|
|
26
|
+
: ["--import", "tsx/esm", sourceEntry];
|
|
27
|
+
|
|
28
|
+
const child = spawn(process.execPath, [...nodeArgs, ...process.argv.slice(2)], {
|
|
29
|
+
cwd: process.cwd(),
|
|
30
|
+
env: process.env,
|
|
31
|
+
stdio: "inherit"
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
child.on("exit", (code, signal) => {
|
|
35
|
+
if (signal) {
|
|
36
|
+
process.kill(process.pid, signal);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.exit(code ?? 1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
child.on("error", (error) => {
|
|
44
|
+
console.error("Failed to launch fvtt-world-cli", error);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cpSync, rmSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const packageDirectory = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
+
const repositoryRoot = resolve(packageDirectory, "../..");
|
|
7
|
+
const assets = ["LICENSE", "README.md", "docs", "skills"];
|
|
8
|
+
|
|
9
|
+
const clean = process.argv.includes("--clean");
|
|
10
|
+
|
|
11
|
+
for (const asset of assets) {
|
|
12
|
+
const destination = join(packageDirectory, asset);
|
|
13
|
+
rmSync(destination, { recursive: true, force: true });
|
|
14
|
+
if (clean) continue;
|
|
15
|
+
|
|
16
|
+
const source = join(repositoryRoot, asset);
|
|
17
|
+
if (!statSync(source, { throwIfNoEntry: false })) {
|
|
18
|
+
console.error(`Missing ${source}; cannot package the ${asset} assets.`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
cpSync(source, destination, { recursive: true });
|
|
22
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
}
|