openalmanac 0.2.56 → 0.2.57
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/server.js +46 -1
- package/dist/tools/pages.js +15 -12
- package/dist/tools/topics.js +4 -2
- package/dist/tools/users.d.ts +2 -0
- package/dist/tools/users.js +13 -0
- package/dist/tools/wikis.js +57 -18
- package/dist/validate.d.ts +960 -0
- package/dist/validate.js +136 -113
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { registerPageTools } from "./tools/pages.js";
|
|
|
7
7
|
import { registerResearchTools } from "./tools/research.js";
|
|
8
8
|
import { registerWikiTools } from "./tools/wikis.js";
|
|
9
9
|
import { registerTopicTools } from "./tools/topics.js";
|
|
10
|
+
import { registerUserTools } from "./tools/users.js";
|
|
10
11
|
import { getApiKey } from "./auth.js";
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
@@ -122,11 +123,54 @@ export function createServer() {
|
|
|
122
123
|
"",
|
|
123
124
|
"Why this order: the draft must be finished before subagents run. The linking agent needs to see what entities are actually in the text. The image agent needs to match images to specific content. The review agent needs the complete article. Everything reads the draft.",
|
|
124
125
|
"",
|
|
126
|
+
"## Wikilink syntax",
|
|
127
|
+
"",
|
|
128
|
+
"Wikilinks are resolved server-side at publish time. There are four forms — every one uses double brackets:",
|
|
129
|
+
"",
|
|
130
|
+
"- `[[slug]]` — link to another page in the SAME wiki you're publishing to. Display text is the page's title.",
|
|
131
|
+
"- `[[slug|Display text]]` — same wiki, custom display text.",
|
|
132
|
+
"- `[[global:slug]]` / `[[global:slug|Display]]` — link to a page in the global almanac (the shared wiki with slug `global`). Use this for cross-cutting entities (people, technologies, concepts) that belong in the global knowledge base rather than a per-topic wiki.",
|
|
133
|
+
"- `[[wiki-slug:page-slug]]` / `[[wiki-slug:page-slug|Display]]` — link to a specific page in another wiki.",
|
|
134
|
+
"",
|
|
135
|
+
"Dead links auto-create stub pages on publish — you can link to entities that don't exist yet and the server will scaffold empty pages at those slugs so the links resolve. Use `resolve` before publish if you want to see which targets are `found` / `stub` / `not_found`.",
|
|
136
|
+
"",
|
|
137
|
+
"Inline mentions without brackets are NOT linked. If you want an entity to be linkable, wrap it in `[[...]]`.",
|
|
138
|
+
"",
|
|
139
|
+
"## Authoring an infobox",
|
|
140
|
+
"",
|
|
141
|
+
"Infoboxes have a strict pydantic schema on the backend — unknown section types, bare-string `header.links`, int values in `header.details`, and missing `primary` on timeline items are all rejected at publish time with a 400. Before authoring ANY infobox, fetch the full field-by-field reference:",
|
|
142
|
+
"",
|
|
143
|
+
" https://www.openalmanac.org/infobox-schema.md",
|
|
144
|
+
"",
|
|
145
|
+
"Follow it exactly. Do not invent new section types or field names — if the schema doesn't describe what you want, fold the information into `key_value` or `list`. The six valid section types are `timeline`, `list`, `tags`, `grid`, `table`, `key_value`.",
|
|
146
|
+
"",
|
|
147
|
+
"## Working across wikis",
|
|
148
|
+
"",
|
|
149
|
+
"OpenAlmanac is multi-wiki. Every page lives inside one wiki. Before writing or creating anything, ground yourself:",
|
|
150
|
+
"",
|
|
151
|
+
"- `whoami` → who is the user? Needed to address them and to reason about \"my wikis\".",
|
|
152
|
+
"- `list_wikis` → what wikis exist? Use this BEFORE `create_wiki` so you can suggest contributing to an existing wiki instead of spinning up a parallel one.",
|
|
153
|
+
"",
|
|
154
|
+
"### Creating a new wiki — collaborative flow",
|
|
155
|
+
"",
|
|
156
|
+
"When the user says \"I want to start a wiki about X\", don't just call `create_wiki` and dump content. Do this:",
|
|
157
|
+
"",
|
|
158
|
+
"1. **Check what's there.** Call `list_wikis` to see existing wikis. If something close exists, surface it: \"There's already a `<slug>` wiki on a related area — do you want to contribute there, or is this a distinct enough angle?\"",
|
|
159
|
+
"2. **Research the space briefly.** Use `search_web` + `read_webpage` to get real information about the topic. A few quick reads, not a deep dive — enough to brainstorm coherently.",
|
|
160
|
+
"3. **Brainstorm structure with the user.** Be conversational, not a form-filler. Propose a few natural topics the wiki might have (3-6 bullet points max), what the scope is, what the voice/angle is. Ask what resonates. Don't write prose yet.",
|
|
161
|
+
"4. **Create the wiki.** `create_wiki` with title + description. The server auto-scaffolds a `main-page` with default homepage directives.",
|
|
162
|
+
"5. **Edit the main page in place.** `download` the auto-created `main-page` and edit the file (don't `new` a fresh `main-page` — the server derives slug from title, so a new scaffold would get a different slug and you'd end up with two homepages).",
|
|
163
|
+
"6. **Seed the topic hierarchy.** `create_topics_batch` with the topics you agreed on.",
|
|
164
|
+
"7. **Wire navigation.** `update_wiki_settings` with a `nav` array. Each NavItem needs exactly one of `page` / `topic` / `link`. Use `auto: {enabled: true}` on topic NavItems to auto-populate children from the topic DAG.",
|
|
165
|
+
"8. **Seed a few stub pages or first articles.** Stubs are fine — scaffold-and-fill-later is a supported workflow.",
|
|
166
|
+
"",
|
|
167
|
+
"The conversation drives the shape of the wiki. Don't over-engineer the topic hierarchy or the nav on turn one. Ship a small coherent starting shape and grow it with the user.",
|
|
168
|
+
"",
|
|
125
169
|
"## Technical workflow",
|
|
126
170
|
"",
|
|
127
171
|
"Reading and searching articles is open. Writing requires an API key (from login). Login creates a personal API key linked to your user account, so contributions are attributed to you.",
|
|
128
172
|
"",
|
|
129
|
-
"Core flow: login (once) → search_articles (
|
|
173
|
+
"Core flow: login (once) → `whoami` (confirm identity) → `list_wikis` or `search_articles` (what exists?) → `search_web` + `read_webpage` (research) → `new` (scaffold) or `download` (existing) → edit files under ~/.openalmanac/articles/{wiki_slug}/ → `publish`.",
|
|
130
174
|
"",
|
|
131
175
|
"After publishing, share the celebration URL when applicable. Use `list_articles` with `wiki_slug` to browse a wiki's pages.",
|
|
132
176
|
"",
|
|
@@ -138,5 +182,6 @@ export function createServer() {
|
|
|
138
182
|
registerResearchTools(server);
|
|
139
183
|
registerWikiTools(server);
|
|
140
184
|
registerTopicTools(server);
|
|
185
|
+
registerUserTools(server);
|
|
141
186
|
return server;
|
|
142
187
|
}
|
package/dist/tools/pages.js
CHANGED
|
@@ -93,19 +93,22 @@ export function registerPageTools(server) {
|
|
|
93
93
|
include_stubs: z.boolean().default(true).describe("Include stubs in results"),
|
|
94
94
|
}),
|
|
95
95
|
async execute({ queries, wiki, limit, include_stubs }) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
96
|
+
// Single server round-trip via /api/search/batch (rate-limited 10/min).
|
|
97
|
+
// Backend fans out internally and returns [{query, results[], error?}, ...].
|
|
98
|
+
const body = {
|
|
99
|
+
queries,
|
|
100
|
+
limit,
|
|
101
|
+
include_stubs,
|
|
102
|
+
};
|
|
103
|
+
if (wiki)
|
|
104
|
+
body.wiki = wiki;
|
|
105
|
+
const resp = await request("POST", "/api/search/batch", { json: body });
|
|
106
|
+
const data = (await resp.json());
|
|
107
|
+
const byQuery = {};
|
|
108
|
+
for (const set of data.results) {
|
|
109
|
+
byQuery[set.query] = set.error ? { error: set.error } : set.results;
|
|
107
110
|
}
|
|
108
|
-
return JSON.stringify(
|
|
111
|
+
return JSON.stringify(byQuery, null, 2);
|
|
109
112
|
},
|
|
110
113
|
});
|
|
111
114
|
server.addTool({
|
package/dist/tools/topics.js
CHANGED
|
@@ -35,12 +35,13 @@ export function registerTopicTools(server) {
|
|
|
35
35
|
wiki_slug: z.string().describe("Wiki slug"),
|
|
36
36
|
title: z.string().describe("Topic title"),
|
|
37
37
|
description: z.string().default("").describe("Topic description"),
|
|
38
|
+
image_url: z.string().url().max(2048).optional().describe("Topic image URL (https:// or http://)"),
|
|
38
39
|
parent_slugs: coerceJson(z.array(z.string())).default([]).describe("Parent topic slugs"),
|
|
39
40
|
}),
|
|
40
|
-
async execute({ wiki_slug, title, description, parent_slugs }) {
|
|
41
|
+
async execute({ wiki_slug, title, description, image_url, parent_slugs }) {
|
|
41
42
|
const resp = await request("POST", `/api/w/${wiki_slug}/topics`, {
|
|
42
43
|
auth: true,
|
|
43
|
-
json: { title, description, parent_slugs },
|
|
44
|
+
json: { title, description, image_url, parent_slugs },
|
|
44
45
|
});
|
|
45
46
|
return JSON.stringify(await resp.json(), null, 2);
|
|
46
47
|
},
|
|
@@ -53,6 +54,7 @@ export function registerTopicTools(server) {
|
|
|
53
54
|
topics: coerceJson(z.array(z.object({
|
|
54
55
|
title: z.string(),
|
|
55
56
|
description: z.string().default(""),
|
|
57
|
+
image_url: z.string().url().max(2048).optional(),
|
|
56
58
|
parent_slugs: z.array(z.string()).default([]),
|
|
57
59
|
})).min(1).max(100)).describe("Topics to create"),
|
|
58
60
|
}),
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { request } from "../auth.js";
|
|
3
|
+
export function registerUserTools(server) {
|
|
4
|
+
server.addTool({
|
|
5
|
+
name: "whoami",
|
|
6
|
+
description: "Return the current user's profile — username, display name, avatar. Use this to ground any \"my X\" decision (which wikis am I a member of, how should I address the user, etc.) before making assumptions. Requires login.",
|
|
7
|
+
parameters: z.object({}),
|
|
8
|
+
async execute() {
|
|
9
|
+
const resp = await request("GET", "/api/users/me", { auth: true });
|
|
10
|
+
return JSON.stringify(await resp.json(), null, 2);
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
}
|
package/dist/tools/wikis.js
CHANGED
|
@@ -13,10 +13,61 @@ function coerceJson(schema) {
|
|
|
13
13
|
return val;
|
|
14
14
|
}, schema);
|
|
15
15
|
}
|
|
16
|
+
// Mirrors backend `NavItem` in src/schemas/wiki_settings_schemas.py. The
|
|
17
|
+
// refinement matches the `exactly_one_target` @model_validator there —
|
|
18
|
+
// agents get a clear error pre-flight instead of a 422 round-trip.
|
|
19
|
+
const navItemSchema = z.lazy(() => z.object({
|
|
20
|
+
label: z.string(),
|
|
21
|
+
page: z.string().optional(),
|
|
22
|
+
topic: z.string().optional(),
|
|
23
|
+
link: z.string().optional(),
|
|
24
|
+
children: z.array(navItemSchema).optional(),
|
|
25
|
+
auto: z.object({
|
|
26
|
+
enabled: z.boolean(),
|
|
27
|
+
include_subtopics: z.boolean().optional(),
|
|
28
|
+
include_pages: z.boolean().optional(),
|
|
29
|
+
subtopic_limit: z.number().int().positive().optional(),
|
|
30
|
+
page_limit: z.number().int().positive().optional(),
|
|
31
|
+
show_view_all: z.boolean().optional(),
|
|
32
|
+
hidden_topic_slugs: z.array(z.string()).optional(),
|
|
33
|
+
hidden_page_slugs: z.array(z.string()).optional(),
|
|
34
|
+
pinned: z.array(z.object({
|
|
35
|
+
kind: z.enum(["topic", "page"]),
|
|
36
|
+
slug: z.string(),
|
|
37
|
+
})).optional(),
|
|
38
|
+
}).optional(),
|
|
39
|
+
}).refine((item) => {
|
|
40
|
+
const targets = [item.page, item.topic, item.link].filter((t) => typeof t === "string" && t.trim().length > 0);
|
|
41
|
+
return targets.length === 1;
|
|
42
|
+
}, { message: "NavItem must have exactly one of page, topic, or link" }).refine((item) => !(item.auto && !item.topic), { message: "NavItem auto mode requires a topic target" }).refine((item) => !(item.auto && item.children && item.children.length > 0), { message: "NavItem cannot define children while auto mode is enabled" }));
|
|
43
|
+
// Mirrors backend `WikiTheme`. All fields are optional from the MCP side —
|
|
44
|
+
// the backend fills defaults. Extra keys are dropped (backend uses
|
|
45
|
+
// model_config extra="ignore").
|
|
46
|
+
const themeSchema = z.object({
|
|
47
|
+
accent_color: z.string().optional(),
|
|
48
|
+
name_font: z.string().optional(),
|
|
49
|
+
logo_url: z.string().optional(),
|
|
50
|
+
cover_tint_intensity: z.number().optional(),
|
|
51
|
+
logo_tint_intensity: z.number().optional(),
|
|
52
|
+
cover_y_offset: z.number().optional(),
|
|
53
|
+
}).passthrough();
|
|
16
54
|
export function registerWikiTools(server) {
|
|
55
|
+
server.addTool({
|
|
56
|
+
name: "list_wikis",
|
|
57
|
+
description: "List every wiki on OpenAlmanac. Use before creating a new wiki so you can suggest contributing to an existing one. The global almanac has slug `global` and is excluded by default — pass `include_global: true` to include it. No authentication needed.",
|
|
58
|
+
parameters: z.object({
|
|
59
|
+
include_global: z.boolean().default(false).describe("Include the global almanac wiki in results"),
|
|
60
|
+
}),
|
|
61
|
+
async execute({ include_global }) {
|
|
62
|
+
const resp = await request("GET", "/api/wikis", {
|
|
63
|
+
params: { include_global },
|
|
64
|
+
});
|
|
65
|
+
return JSON.stringify(await resp.json(), null, 2);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
17
68
|
server.addTool({
|
|
18
69
|
name: "create_wiki",
|
|
19
|
-
description: "Create a new wiki. Returns the wiki slug and URL. Requires login.",
|
|
70
|
+
description: "Create a new wiki. Returns the wiki slug and URL. A welcome `main-page` is auto-created with default homepage directives — download it and edit in place rather than scaffolding a fresh `main-page` (the server derives slug from title, so a new scaffold would get a different slug). Requires login.",
|
|
20
71
|
parameters: z.object({
|
|
21
72
|
title: z.string().describe("Wiki title"),
|
|
22
73
|
description: z.string().default("").describe("Wiki description"),
|
|
@@ -27,7 +78,7 @@ export function registerWikiTools(server) {
|
|
|
27
78
|
json: { title, description },
|
|
28
79
|
});
|
|
29
80
|
const wiki = (await resp.json());
|
|
30
|
-
return `Created wiki "${wiki.title}" at /w/${wiki.slug}
|
|
81
|
+
return `Created wiki "${wiki.title}" at /w/${wiki.slug}. The homepage was auto-scaffolded at slug "main-page" — download it to edit.`;
|
|
31
82
|
},
|
|
32
83
|
});
|
|
33
84
|
server.addTool({
|
|
@@ -43,19 +94,13 @@ export function registerWikiTools(server) {
|
|
|
43
94
|
});
|
|
44
95
|
server.addTool({
|
|
45
96
|
name: "update_wiki_settings",
|
|
46
|
-
description: "Update a wiki's settings (nav, cover_image_url, theme). Requires moderator access.",
|
|
97
|
+
description: "Update a wiki's settings (nav, cover_image_url, theme). Each NavItem must have exactly one of `page`, `topic`, or `link`. Use `auto` (only on topic items) to auto-populate children from the topic DAG. Requires moderator access.",
|
|
47
98
|
parameters: z.object({
|
|
48
99
|
wiki_slug: z.string().describe("Wiki slug"),
|
|
49
100
|
settings: coerceJson(z.object({
|
|
50
|
-
nav: z.array(
|
|
51
|
-
label: z.string(),
|
|
52
|
-
page: z.string().optional(),
|
|
53
|
-
topic: z.string().optional(),
|
|
54
|
-
link: z.string().optional(),
|
|
55
|
-
children: z.array(z.any()).optional(),
|
|
56
|
-
})).optional(),
|
|
101
|
+
nav: z.array(navItemSchema).optional(),
|
|
57
102
|
cover_image_url: z.string().optional(),
|
|
58
|
-
theme:
|
|
103
|
+
theme: themeSchema.optional(),
|
|
59
104
|
})).describe("Settings to update"),
|
|
60
105
|
}),
|
|
61
106
|
async execute({ wiki_slug, settings }) {
|
|
@@ -71,13 +116,7 @@ export function registerWikiTools(server) {
|
|
|
71
116
|
description: "Update just the navigation tree for a wiki. Shorthand for updating settings.nav. Requires moderator access.",
|
|
72
117
|
parameters: z.object({
|
|
73
118
|
wiki_slug: z.string().describe("Wiki slug"),
|
|
74
|
-
nav: coerceJson(z.array(
|
|
75
|
-
label: z.string(),
|
|
76
|
-
page: z.string().optional(),
|
|
77
|
-
topic: z.string().optional(),
|
|
78
|
-
link: z.string().optional(),
|
|
79
|
-
children: z.array(z.any()).optional(),
|
|
80
|
-
}))).describe("Nav items"),
|
|
119
|
+
nav: coerceJson(z.array(navItemSchema)).describe("Nav items"),
|
|
81
120
|
}),
|
|
82
121
|
async execute({ wiki_slug, nav }) {
|
|
83
122
|
// Get current settings, merge nav
|