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 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
+ [![Foundry VTT v13–v14](https://img.shields.io/badge/Foundry%20VTT-v13%E2%80%93v14-ff6400)](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
+ }