world-building-framework 0.2.0 → 0.2.2
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 +46 -3
- package/dist/cli/commands/create.d.ts +1 -7
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.js +8 -4
- package/dist/cli/commands/publish.js +1 -1
- package/dist/cli/commands/refresh.js +5 -1
- package/dist/cli/commands/render.js +1 -1
- package/dist/cli/index.js +18 -1
- package/dist/cli/managed-files.d.ts +11 -1
- package/dist/cli/managed-files.js +27 -5
- package/dist/cli/vault-path.d.ts +1 -1
- package/dist/cli/vault-path.js +5 -25
- package/dist/core/config/schema.d.ts +8 -208
- package/dist/core/config/schema.js +2 -2
- package/dist/core/query/dates.d.ts +4 -40
- package/dist/core/query/types.d.ts +5 -59
- package/dist/core/schema/describe.d.ts +29 -3
- package/dist/core/schema/describe.js +66 -18
- package/dist/core/schema/fields.d.ts +28 -220
- package/dist/core/schema/fields.js +3 -3
- package/dist/core/vault/read.js +6 -2
- package/dist/mcp/server.d.ts +20 -0
- package/dist/mcp/server.js +140 -0
- package/dist/render-pipeline/build.d.ts +5 -2
- package/dist/render-pipeline/build.js +13 -9
- package/dist/render-pipeline/index.d.ts +1 -0
- package/dist/render-pipeline/index.js +1 -0
- package/dist/render-pipeline/slugs.d.ts +3 -2
- package/dist/render-pipeline/slugs.js +4 -3
- package/dist/render-pipeline/start-page.d.ts +9 -0
- package/dist/render-pipeline/start-page.js +44 -0
- package/package.json +3 -2
- package/skills/world-vault/SKILL.md +8 -0
- package/templates/AGENTS.md +9 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { readVaultFile } from "../core/vault/index.js";
|
|
6
|
+
import { runSchema } from "../cli/commands/schema.js";
|
|
7
|
+
import { runSearch } from "../cli/commands/search.js";
|
|
8
|
+
import { runValidate } from "../cli/commands/validate.js";
|
|
9
|
+
import { runCreate } from "../cli/commands/create.js";
|
|
10
|
+
import { runUpdate } from "../cli/commands/update.js";
|
|
11
|
+
import { loadWorldConfig } from "../core/config/index.js";
|
|
12
|
+
import { findVaultDir, resolveVaultDir } from "../cli/vault-path.js";
|
|
13
|
+
/**
|
|
14
|
+
* What every agent connecting to the server is told up front: the
|
|
15
|
+
* world-vault skill's rules (skills/world-vault/SKILL.md), in terms of this
|
|
16
|
+
* server's tools instead of CLI commands.
|
|
17
|
+
*/
|
|
18
|
+
export const MCP_INSTRUCTIONS = `This server reads and writes a world built with the World Building Framework: a vault of entity and event files, named by UUID. Use only these tools to read or change it; they validate everything before it's written.
|
|
19
|
+
|
|
20
|
+
1. Call \`schema\` before writing anything. It lists every field, its shape, and an example input.
|
|
21
|
+
2. Call \`search\` before creating something, to avoid duplicates and to get the UUIDs you link with.
|
|
22
|
+
3. Links are always "[[<uuid>|<current title>]]", in frontmatter and in body text. Never a bare [[Name]].
|
|
23
|
+
4. Before \`create\` or \`update\`, show the user the frontmatter and body you're about to write, and wait for their go-ahead.
|
|
24
|
+
5. \`update\` is for corrections only (a typo, a wrong tag, a missing link). Call \`read\` first and send the file's complete new content: any field you leave out is removed. Changing a title rewrites links showing the old title in other files.
|
|
25
|
+
6. A change in the story is never an update. Create a new entity file with \`supersedes\` (a link to the entity's latest version) and \`causedBy\` (a link to the event that caused the change). Other files keep linking to the entity's first file.
|
|
26
|
+
7. Each published wiki (see \`list_variants\`) includes or leaves out whole files by their tags, and shows an included file in full. Keep secrets in their own files with narrower tags, never in the text of a file a wider audience sees.
|
|
27
|
+
|
|
28
|
+
Errors in a result block a write; warnings don't, but mention them to the user.`;
|
|
29
|
+
function toolResult({ result, failed }) {
|
|
30
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], ...(failed ? { isError: true } : {}) };
|
|
31
|
+
}
|
|
32
|
+
/** Runs a tool body, turning a thrown error into an error result the agent can read, like the CLI's `{"error": ...}`. */
|
|
33
|
+
async function run(body) {
|
|
34
|
+
try {
|
|
35
|
+
return toolResult(await body());
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
return toolResult({ result: { error: err.message }, failed: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** A note when the world pins a different framework version than the one running this server, else undefined. */
|
|
42
|
+
async function versionMismatch(vaultDir, running) {
|
|
43
|
+
if (!vaultDir)
|
|
44
|
+
return undefined;
|
|
45
|
+
try {
|
|
46
|
+
const pkg = JSON.parse(await readFile(path.join(vaultDir, "package.json"), "utf8"));
|
|
47
|
+
const pinned = pkg.dependencies?.["world-building-framework"];
|
|
48
|
+
if (!pinned || pinned === running)
|
|
49
|
+
return undefined;
|
|
50
|
+
return `This server runs world-building-framework ${running}, but the world pins ${pinned}. The published wiki is built with ${pinned}, so validation can differ slightly. Update the world (npm install world-building-framework@${running}, then wbf refresh), or start the server at the pinned version.`;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const fileInput = {
|
|
57
|
+
frontmatter: z.record(z.string(), z.unknown()).describe("The file's frontmatter fields. See the schema tool."),
|
|
58
|
+
body: z.string().optional().describe("Markdown body text. Links inside it use the same [[<uuid>|Title]] form."),
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The `wbf mcp` server (claude-dev/DISTRIBUTION_PLAN.md, component 6): the
|
|
62
|
+
* CLI's vault commands as MCP tools, for agents without a terminal. Each tool
|
|
63
|
+
* calls the same function as its CLI command; only `read` is extra, since an
|
|
64
|
+
* agent without a shell can't open a file to prepare an `update`.
|
|
65
|
+
*/
|
|
66
|
+
export function createMcpServer(options) {
|
|
67
|
+
const server = new McpServer({ name: "world-building-framework", version: options.version }, { instructions: MCP_INSTRUCTIONS });
|
|
68
|
+
const vault = options.vault;
|
|
69
|
+
const readOnly = { readOnlyHint: true, openWorldHint: false };
|
|
70
|
+
server.registerTool("schema", {
|
|
71
|
+
title: "Show the file schema",
|
|
72
|
+
description: "Every frontmatter field with its shape and whether it's required, this world's entityType list, and example inputs for create. Call it before writing anything.",
|
|
73
|
+
annotations: readOnly,
|
|
74
|
+
}, () => run(async () => {
|
|
75
|
+
const text = await runSchema(vault);
|
|
76
|
+
const note = await versionMismatch(findVaultDir(vault), options.version);
|
|
77
|
+
return { result: note ? `${text}\n\nNote: ${note}` : text };
|
|
78
|
+
}));
|
|
79
|
+
server.registerTool("search", {
|
|
80
|
+
title: "Search the world",
|
|
81
|
+
description: "Keyword search over titles, aliases and text. Each result has the file's id (the UUID you link with), title and score, and for entities entityType and tags.",
|
|
82
|
+
inputSchema: {
|
|
83
|
+
query: z.string().min(1).describe("Search terms"),
|
|
84
|
+
tags: z.array(z.string()).optional().describe("Only files carrying all of these tags"),
|
|
85
|
+
limit: z.number().int().positive().optional().describe("Maximum number of results"),
|
|
86
|
+
},
|
|
87
|
+
annotations: readOnly,
|
|
88
|
+
}, ({ query, tags, limit }) => run(async () => ({ result: await runSearch(vault, query, { tags, limit }) })));
|
|
89
|
+
server.registerTool("read", {
|
|
90
|
+
title: "Read a file",
|
|
91
|
+
description: "One file's id, frontmatter and body. Call it before update, which needs the file's complete new content.",
|
|
92
|
+
inputSchema: { id: z.string().describe("The file's UUID") },
|
|
93
|
+
annotations: readOnly,
|
|
94
|
+
}, ({ id }) => run(async () => {
|
|
95
|
+
const file = await readVaultFile(resolveVaultDir(vault), id);
|
|
96
|
+
return { result: { id: file.id, frontmatter: file.frontmatter, body: file.body } };
|
|
97
|
+
}));
|
|
98
|
+
server.registerTool("validate", {
|
|
99
|
+
title: "Validate",
|
|
100
|
+
description: "Checks without writing anything. With frontmatter (and body): a candidate file, exactly as create would check it. With id: one existing file. With neither: the whole world.",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
id: z.string().optional().describe("Check one existing file"),
|
|
103
|
+
frontmatter: fileInput.frontmatter.optional(),
|
|
104
|
+
body: fileInput.body,
|
|
105
|
+
},
|
|
106
|
+
annotations: readOnly,
|
|
107
|
+
}, ({ id, frontmatter, body }) => run(async () => {
|
|
108
|
+
const candidate = frontmatter ? { frontmatter, body } : undefined;
|
|
109
|
+
const outcome = await runValidate(vault, { id: candidate ? undefined : id, candidate });
|
|
110
|
+
const valid = Array.isArray(outcome) ? outcome.every((o) => o.valid) : outcome.valid;
|
|
111
|
+
return { result: outcome, failed: !valid };
|
|
112
|
+
}));
|
|
113
|
+
server.registerTool("create", {
|
|
114
|
+
title: "Create a file",
|
|
115
|
+
description: "Creates a new entity or event file under a new UUID, after the same checks as validate; on any error nothing is written. Returns the new id. Show the user the content and get their go-ahead first.",
|
|
116
|
+
inputSchema: fileInput,
|
|
117
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
118
|
+
}, ({ frontmatter, body }) => run(async () => {
|
|
119
|
+
const outcome = await runCreate(vault, { frontmatter, body });
|
|
120
|
+
return { result: outcome, failed: !outcome.written };
|
|
121
|
+
}));
|
|
122
|
+
server.registerTool("update", {
|
|
123
|
+
title: "Correct a file",
|
|
124
|
+
description: "Replaces an existing file's whole content, for corrections only; a change in the story is a new version (create with supersedes and causedBy). Call read first and send the complete content: fields you leave out are removed. type can't change. A changed title is also rewritten in other files' links. Show the user the change and get their go-ahead first.",
|
|
125
|
+
inputSchema: { id: z.string().describe("The file's UUID"), ...fileInput },
|
|
126
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
127
|
+
}, ({ id, frontmatter, body }) => run(async () => {
|
|
128
|
+
const outcome = await runUpdate(vault, id, { frontmatter, body });
|
|
129
|
+
return { result: outcome, failed: !outcome.written };
|
|
130
|
+
}));
|
|
131
|
+
server.registerTool("list_variants", {
|
|
132
|
+
title: "List the wikis",
|
|
133
|
+
description: "The build variants in world.config.json, by name: one published wiki each, e.g. a player wiki and a GM wiki. A file is in a wiki if it carries all of the variant's tags (and, for events, its date is inside the dateRange); {} includes everything. Use it to see which audiences a file's tags reach.",
|
|
134
|
+
annotations: readOnly,
|
|
135
|
+
}, () => run(async () => {
|
|
136
|
+
const config = await loadWorldConfig(resolveVaultDir(vault));
|
|
137
|
+
return { result: config.buildVariants ?? {} };
|
|
138
|
+
}));
|
|
139
|
+
return server;
|
|
140
|
+
}
|
|
@@ -4,6 +4,9 @@ import type { BuildVariantQuery, RenderedPage } from "./types.js";
|
|
|
4
4
|
* Stage 1: vault -> derived, Quartz-ready page set (RENDER_PIPELINE.md). Filters
|
|
5
5
|
* by the build variant's query, merges each included entity's version history
|
|
6
6
|
* into one page, and rewrites every link to a slug (or strips it if the target
|
|
7
|
-
* isn't included).
|
|
7
|
+
* isn't included). Adds a start page listing everything included, titled
|
|
8
|
+
* `options.title` (the world's title).
|
|
8
9
|
*/
|
|
9
|
-
export declare function buildDerivedTree(files: VaultFile[], query: BuildVariantQuery
|
|
10
|
+
export declare function buildDerivedTree(files: VaultFile[], query: BuildVariantQuery, options?: {
|
|
11
|
+
title?: string;
|
|
12
|
+
}): RenderedPage[];
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { buildGraph } from "../core/graph/index.js";
|
|
1
|
+
import { buildGraph, topologicalOrder } from "../core/graph/index.js";
|
|
2
2
|
import { filterEvents, filterEntityAnchors } from "./filter.js";
|
|
3
3
|
import { mergeEntityBody } from "./merge.js";
|
|
4
4
|
import { assignSlugs } from "./slugs.js";
|
|
5
5
|
import { rewriteLinks, rewriteFrontmatterLinks } from "./rewrite.js";
|
|
6
6
|
import { renameQuartzReservedFields } from "./quartz-compat.js";
|
|
7
|
+
import { buildStartPage, START_PAGE_SLUG } from "./start-page.js";
|
|
7
8
|
function titleOf(file) {
|
|
8
9
|
return typeof file.frontmatter.title === "string" ? file.frontmatter.title : file.id;
|
|
9
10
|
}
|
|
@@ -11,20 +12,22 @@ function titleOf(file) {
|
|
|
11
12
|
* Stage 1: vault -> derived, Quartz-ready page set (RENDER_PIPELINE.md). Filters
|
|
12
13
|
* by the build variant's query, merges each included entity's version history
|
|
13
14
|
* into one page, and rewrites every link to a slug (or strips it if the target
|
|
14
|
-
* isn't included).
|
|
15
|
+
* isn't included). Adds a start page listing everything included, titled
|
|
16
|
+
* `options.title` (the world's title).
|
|
15
17
|
*/
|
|
16
|
-
export function buildDerivedTree(files, query) {
|
|
18
|
+
export function buildDerivedTree(files, query, options = {}) {
|
|
17
19
|
const graph = buildGraph(files);
|
|
18
20
|
const includedEventIds = filterEvents(graph, files, query);
|
|
19
21
|
const includedAnchorIds = filterEntityAnchors(graph, files, query);
|
|
20
22
|
// Sorted for a deterministic slug assignment, independent of vault read order.
|
|
21
23
|
const includedIds = [...includedEventIds, ...includedAnchorIds].sort();
|
|
22
24
|
const titles = new Map(includedIds.map((id) => [id, titleOf(graph.nodes.get(id))]));
|
|
23
|
-
const slugsById = assignSlugs(titles);
|
|
24
|
-
const
|
|
25
|
-
|
|
25
|
+
const slugsById = assignSlugs(titles, [START_PAGE_SLUG]);
|
|
26
|
+
const eventPages = [];
|
|
27
|
+
const entityPages = [];
|
|
28
|
+
for (const id of topologicalOrder(graph).filter((eventId) => includedEventIds.has(eventId))) {
|
|
26
29
|
const file = graph.nodes.get(id);
|
|
27
|
-
|
|
30
|
+
eventPages.push({
|
|
28
31
|
id,
|
|
29
32
|
slug: slugsById.get(id),
|
|
30
33
|
frontmatter: renameQuartzReservedFields(rewriteFrontmatterLinks(file.frontmatter, slugsById)),
|
|
@@ -34,12 +37,13 @@ export function buildDerivedTree(files, query) {
|
|
|
34
37
|
for (const anchorId of includedAnchorIds) {
|
|
35
38
|
const anchor = graph.nodes.get(anchorId);
|
|
36
39
|
const mergedBody = mergeEntityBody(graph, anchorId, includedEventIds);
|
|
37
|
-
|
|
40
|
+
entityPages.push({
|
|
38
41
|
id: anchorId,
|
|
39
42
|
slug: slugsById.get(anchorId),
|
|
40
43
|
frontmatter: renameQuartzReservedFields(rewriteFrontmatterLinks(anchor.frontmatter, slugsById)),
|
|
41
44
|
body: rewriteLinks(mergedBody, slugsById),
|
|
42
45
|
});
|
|
43
46
|
}
|
|
44
|
-
|
|
47
|
+
const startPage = buildStartPage(options.title ?? "World Wiki", entityPages, eventPages);
|
|
48
|
+
return [...eventPages, ...entityPages, startPage];
|
|
45
49
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Assigns a unique, readable slug per id — vault files are UUID-named (SCHEMA.md),
|
|
3
3
|
* meaningless as a published URL; this is what Stage 1 uses instead for the
|
|
4
4
|
* derived tree. Iterates `titles` in whatever order the caller provides, so pass
|
|
5
|
-
* ids in a deterministic order for reproducible builds.
|
|
5
|
+
* ids in a deterministic order for reproducible builds. No page gets a slug
|
|
6
|
+
* in `reserved` (e.g. the start page's "index").
|
|
6
7
|
*/
|
|
7
|
-
export declare function assignSlugs(titles: Map<string, string
|
|
8
|
+
export declare function assignSlugs(titles: Map<string, string>, reserved?: string[]): Map<string, string>;
|
|
@@ -10,10 +10,11 @@ function slugify(title) {
|
|
|
10
10
|
* Assigns a unique, readable slug per id — vault files are UUID-named (SCHEMA.md),
|
|
11
11
|
* meaningless as a published URL; this is what Stage 1 uses instead for the
|
|
12
12
|
* derived tree. Iterates `titles` in whatever order the caller provides, so pass
|
|
13
|
-
* ids in a deterministic order for reproducible builds.
|
|
13
|
+
* ids in a deterministic order for reproducible builds. No page gets a slug
|
|
14
|
+
* in `reserved` (e.g. the start page's "index").
|
|
14
15
|
*/
|
|
15
|
-
export function assignSlugs(titles) {
|
|
16
|
-
const used = new Set();
|
|
16
|
+
export function assignSlugs(titles, reserved = []) {
|
|
17
|
+
const used = new Set(reserved);
|
|
17
18
|
const slugs = new Map();
|
|
18
19
|
for (const [id, title] of titles) {
|
|
19
20
|
const base = slugify(title);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RenderedPage } from "./types.js";
|
|
2
|
+
/** Quartz builds a site's root page from `content/index.md`; without it, the wiki's own address is a 404. */
|
|
3
|
+
export declare const START_PAGE_SLUG = "index";
|
|
4
|
+
/**
|
|
5
|
+
* The wiki's start page: every included entity, grouped by entityType and sorted
|
|
6
|
+
* by title, then every included event in causal order. `events` must already be
|
|
7
|
+
* in that order; only pages of this build go in, so nothing filtered out shows up.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildStartPage(title: string, entities: RenderedPage[], events: RenderedPage[]): RenderedPage;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Quartz builds a site's root page from `content/index.md`; without it, the wiki's own address is a 404. */
|
|
2
|
+
export const START_PAGE_SLUG = "index";
|
|
3
|
+
function titleOf(page) {
|
|
4
|
+
return typeof page.frontmatter.title === "string" ? page.frontmatter.title : page.slug;
|
|
5
|
+
}
|
|
6
|
+
function linkTo(page) {
|
|
7
|
+
return `[[${page.slug}|${titleOf(page)}]]`;
|
|
8
|
+
}
|
|
9
|
+
function describeDate(page) {
|
|
10
|
+
const date = page.frontmatter.worldDate;
|
|
11
|
+
if (typeof date?.year !== "number")
|
|
12
|
+
return "";
|
|
13
|
+
return typeof date.day === "number" ? ` (year ${date.year}, day ${date.day})` : ` (year ${date.year})`;
|
|
14
|
+
}
|
|
15
|
+
function heading(entityType) {
|
|
16
|
+
return entityType.charAt(0).toUpperCase() + entityType.slice(1);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The wiki's start page: every included entity, grouped by entityType and sorted
|
|
20
|
+
* by title, then every included event in causal order. `events` must already be
|
|
21
|
+
* in that order; only pages of this build go in, so nothing filtered out shows up.
|
|
22
|
+
*/
|
|
23
|
+
export function buildStartPage(title, entities, events) {
|
|
24
|
+
const groups = new Map();
|
|
25
|
+
for (const page of entities) {
|
|
26
|
+
const entityType = typeof page.frontmatter.entityType === "string" ? page.frontmatter.entityType : "";
|
|
27
|
+
groups.set(entityType, [...(groups.get(entityType) ?? []), page]);
|
|
28
|
+
}
|
|
29
|
+
// Typed groups alphabetically, untyped entities last.
|
|
30
|
+
const groupKeys = [...groups.keys()].sort((a, b) => (a === "" ? 1 : b === "" ? -1 : a.localeCompare(b)));
|
|
31
|
+
const sections = groupKeys.map((key) => {
|
|
32
|
+
const pages = groups.get(key).sort((a, b) => titleOf(a).localeCompare(titleOf(b)));
|
|
33
|
+
return `## ${key === "" ? "Other" : heading(key)}\n\n${pages.map((page) => `- ${linkTo(page)}`).join("\n")}`;
|
|
34
|
+
});
|
|
35
|
+
if (events.length > 0) {
|
|
36
|
+
sections.push(`## Events\n\n${events.map((page) => `- ${linkTo(page)}${describeDate(page)}`).join("\n")}`);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
id: START_PAGE_SLUG,
|
|
40
|
+
slug: START_PAGE_SLUG,
|
|
41
|
+
frontmatter: { title },
|
|
42
|
+
body: sections.length > 0 ? `${sections.join("\n\n")}\n` : "Nothing here yet.\n",
|
|
43
|
+
};
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "world-building-framework",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Build, search and publish fictional worlds from a vault of Markdown files: the wbf CLI and its core engine.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -32,11 +32,12 @@
|
|
|
32
32
|
"prepublishOnly": "npm run typecheck && npm test"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
35
36
|
"better-sqlite3": "^13.0.3",
|
|
36
37
|
"commander": "^15.0.0",
|
|
37
38
|
"gray-matter": "^4.0.3",
|
|
38
39
|
"uuid": "^14.0.2",
|
|
39
|
-
"zod": "^
|
|
40
|
+
"zod": "^4.6.7"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@types/better-sqlite3": "^9.6.0",
|
|
@@ -98,3 +98,11 @@ When an event changes something about an entity (it looks different, its status
|
|
|
98
98
|
Both fields are required together. The vault keeps every version, and the published wiki shows the whole history. Never overwrite an entity file's body to reflect an in-world change; that erases history the vault is designed to keep.
|
|
99
99
|
|
|
100
100
|
Other files always link to an entity's **original, first-ever** file (its anchor), never to a later version. If you're not sure which id is the anchor, follow the file's `supersedes` chain back. Or reuse whatever id you already have for that entity: that's always the anchor once created.
|
|
101
|
+
|
|
102
|
+
## 9. Secrets: each wiki shows whole files
|
|
103
|
+
|
|
104
|
+
A world can publish several wikis (build variants in `world.config.json`, e.g. a player wiki and a GM wiki). They filter by **file**, using its `tags` and, for events, its date. Everything inside a file that's included is shown, body text and all.
|
|
105
|
+
|
|
106
|
+
- Before writing, check which wikis the file will appear in: compare its `tags` with each variant's `tags` in `world.config.json`.
|
|
107
|
+
- Keep anything meant for fewer readers (a GM secret, a later plot twist) out of files the wider audience sees. Put it in its own file with narrower tags, and link to it from there.
|
|
108
|
+
- A link to a file a wiki leaves out turns into plain text, so its title still shows. Give secret files titles that don't give the secret away.
|
package/templates/AGENTS.md
CHANGED
|
@@ -6,6 +6,14 @@ This repo is a world built with the World Building Framework (version __WBF_VERS
|
|
|
6
6
|
- Run `npx wbf schema` before writing anything, `npx wbf search <terms>` before creating something, and `npx wbf --help` for every command.
|
|
7
7
|
- Never hand-write or hand-edit a vault `.md` file. Only `wbf create` and `wbf update` validate content, and only `create` generates the required UUID filename.
|
|
8
8
|
- Show the user what you're about to write, and wait for their go-ahead, before `create` or `update`.
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
## Skills
|
|
11
|
+
|
|
12
|
+
The `world-vault` skill (open Agent Skills format, `SKILL.md`) has the full workflow. If your project skills folder doesn't have it yet, install it for yourself, then tell the user to commit the new files:
|
|
13
|
+
|
|
14
|
+
- Claude Code: `npx wbf init --agent claude`
|
|
15
|
+
- Any other agent that reads `SKILL.md` skills: `npx wbf init --skills-dir <your project skills folder>`, relative to this repo. Only use a folder you know your agent reads.
|
|
16
|
+
|
|
17
|
+
`init` only adds what's missing here and never overwrites a file.
|
|
10
18
|
|
|
11
19
|
Generated by `wbf init`. `npx wbf refresh` keeps it current while it's unedited.
|