openalmanac 0.2.10 → 0.2.12
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 +26 -4
- package/dist/tools/articles.js +71 -6
- package/dist/tools/people.d.ts +2 -0
- package/dist/tools/people.js +20 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -3,13 +3,24 @@ import { registerAuthTools } from "./tools/auth.js";
|
|
|
3
3
|
import { registerArticleTools } from "./tools/articles.js";
|
|
4
4
|
import { registerResearchTools } from "./tools/research.js";
|
|
5
5
|
import { registerCommunityTools } from "./tools/communities.js";
|
|
6
|
+
import { registerPeopleTools } from "./tools/people.js";
|
|
6
7
|
import { getApiKey } from "./auth.js";
|
|
7
|
-
import { openBrowser } from "./browser.js";
|
|
8
|
-
const NEXT_STEPS_URL = "https://www.openalmanac.org/contribute/next-steps";
|
|
9
8
|
export function createServer() {
|
|
10
|
-
// On first run (no API key), open the next-steps page so the user knows what to do
|
|
11
9
|
if (!getApiKey()) {
|
|
12
|
-
|
|
10
|
+
console.error(`
|
|
11
|
+
┌──────────────────────────────────────────────────┐
|
|
12
|
+
│ │
|
|
13
|
+
│ Welcome to OpenAlmanac │
|
|
14
|
+
│ │
|
|
15
|
+
│ Try asking your agent: │
|
|
16
|
+
│ │
|
|
17
|
+
│ → "Write an Almanac article about CORS" │
|
|
18
|
+
│ → "Improve the Alan Turing article" │
|
|
19
|
+
│ │
|
|
20
|
+
│ Docs: openalmanac.org/contribute │
|
|
21
|
+
│ │
|
|
22
|
+
└──────────────────────────────────────────────────┘
|
|
23
|
+
`);
|
|
13
24
|
}
|
|
14
25
|
const server = new FastMCP({
|
|
15
26
|
name: "OpenAlmanac",
|
|
@@ -24,6 +35,16 @@ export function createServer() {
|
|
|
24
35
|
"Before writing or editing any article, read https://www.openalmanac.org/ai-patterns-to-avoid.md " +
|
|
25
36
|
"— it covers AI writing patterns that erode trust (inflated significance, promotional language, " +
|
|
26
37
|
"formulaic conclusions, etc.). Every sentence should contain a specific fact the reader didn't know.\n\n" +
|
|
38
|
+
"Before writing articles with entity links, read https://www.openalmanac.org/ai-linking-guidelines.md " +
|
|
39
|
+
"— it covers wikilink syntax, slug conventions, and stub creation requirements.\n\n" +
|
|
40
|
+
"## Article Linking\n\n" +
|
|
41
|
+
"Every entity mentioned in an article should have a stub or article. Use [[slug|Display Text]] " +
|
|
42
|
+
"syntax for internal links. Before writing a [[link]], confirm the slug exists via search_articles " +
|
|
43
|
+
"or create it via create_stub. Links to non-existent slugs are stripped to plain text on push.\n\n" +
|
|
44
|
+
"Workflow: For each entity mentioned → search_articles (check if exists) → if not, create_stub " +
|
|
45
|
+
"with title, entity_type, headline, and a 2-4 sentence summary → then use [[slug|Display Text]] in your article.\n\n" +
|
|
46
|
+
"For people: call search_people to find their LinkedIn-backed slug, then create_stub. " +
|
|
47
|
+
"For topics/orgs/events: use descriptive kebab-case slugs (e.g. 'reinforcement-learning', 'openai').\n\n" +
|
|
27
48
|
"After creating an article, always share the exact URL from the push response with the user. " +
|
|
28
49
|
"This URL includes a celebration page for the newly created article.\n\n" +
|
|
29
50
|
"After pushing an article, call search_communities to find relevant communities. " +
|
|
@@ -37,5 +58,6 @@ export function createServer() {
|
|
|
37
58
|
registerArticleTools(server);
|
|
38
59
|
registerResearchTools(server);
|
|
39
60
|
registerCommunityTools(server);
|
|
61
|
+
registerPeopleTools(server);
|
|
40
62
|
return server;
|
|
41
63
|
}
|
package/dist/tools/articles.js
CHANGED
|
@@ -98,19 +98,79 @@ function ensureArticlesDir() {
|
|
|
98
98
|
export function registerArticleTools(server) {
|
|
99
99
|
server.addTool({
|
|
100
100
|
name: "search_articles",
|
|
101
|
-
description: "Search existing OpenAlmanac articles. Use this to check if an article already exists before creating one.
|
|
101
|
+
description: "Search existing OpenAlmanac articles and stubs. Use this to check if an article or stub already exists before creating one. " +
|
|
102
|
+
"Results include a 'stub' field (true/false) and 'entity_type' field. No authentication needed.",
|
|
102
103
|
parameters: z.object({
|
|
103
104
|
query: z.string().describe("Search terms"),
|
|
104
105
|
limit: z.number().default(10).describe("Max results (1-100, default 10)"),
|
|
105
106
|
page: z.number().default(1).describe("Page number, 1-indexed (default 1)"),
|
|
107
|
+
include_stubs: z.boolean().default(true).describe("Include stub articles in results (default true)"),
|
|
106
108
|
}),
|
|
107
|
-
async execute({ query, limit, page }) {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
async execute({ query, limit, page, include_stubs }) {
|
|
110
|
+
const params = { query, limit, page };
|
|
111
|
+
if (!include_stubs)
|
|
112
|
+
params.include_stubs = "false";
|
|
113
|
+
const resp = await request("GET", "/api/search", { params });
|
|
111
114
|
return JSON.stringify(await resp.json(), null, 2);
|
|
112
115
|
},
|
|
113
116
|
});
|
|
117
|
+
server.addTool({
|
|
118
|
+
name: "create_stub",
|
|
119
|
+
description: "Create a stub article — a placeholder for an entity that doesn't have a full article yet. " +
|
|
120
|
+
"Use this for every entity (person, organization, topic, etc.) you mention in an article. " +
|
|
121
|
+
"Stubs should include a meaningful summary and headline. " +
|
|
122
|
+
"Idempotent: if the slug already exists, returns the existing article/stub. Requires login.",
|
|
123
|
+
parameters: z.object({
|
|
124
|
+
slug: z
|
|
125
|
+
.string()
|
|
126
|
+
.min(1)
|
|
127
|
+
.max(500)
|
|
128
|
+
.describe("Unique kebab-case identifier. For people with LinkedIn: use their vanity ID (e.g. 'john-smith-4a8b2c1'). " +
|
|
129
|
+
"For others: descriptive kebab-case (e.g. 'reinforcement-learning', 'openai')"),
|
|
130
|
+
title: z.string().describe("Display title (e.g. 'John Smith', 'Reinforcement Learning')"),
|
|
131
|
+
entity_type: z
|
|
132
|
+
.enum(["person", "organization", "topic", "event", "creative_work", "place"])
|
|
133
|
+
.optional()
|
|
134
|
+
.describe("Entity type: 'person' for individuals, 'organization' for companies/institutions/nonprofits, " +
|
|
135
|
+
"'topic' for concepts/fields/technologies, 'event' for conferences/historical events, " +
|
|
136
|
+
"'creative_work' for books/papers/films/software, 'place' for cities/countries/landmarks"),
|
|
137
|
+
headline: z
|
|
138
|
+
.string()
|
|
139
|
+
.optional()
|
|
140
|
+
.describe("Short headline (e.g. 'Professor of CS at MIT', 'AI research laboratory')"),
|
|
141
|
+
image_url: z.string().optional().describe("Image URL for the entity"),
|
|
142
|
+
summary: z
|
|
143
|
+
.string()
|
|
144
|
+
.optional()
|
|
145
|
+
.describe("2-4 sentence summary of the entity. This becomes the stub page content. " +
|
|
146
|
+
"Be informative — include key facts, dates, and context."),
|
|
147
|
+
}),
|
|
148
|
+
async execute({ slug, title, entity_type, headline, image_url, summary }) {
|
|
149
|
+
if (!SLUG_RE.test(slug)) {
|
|
150
|
+
throw new Error(`Invalid slug "${slug}". Must be kebab-case (e.g. 'machine-learning'). Pattern: ^[a-z0-9]+(-[a-z0-9]+)*$`);
|
|
151
|
+
}
|
|
152
|
+
const json = { slug, title };
|
|
153
|
+
if (entity_type)
|
|
154
|
+
json.entity_type = entity_type;
|
|
155
|
+
if (headline)
|
|
156
|
+
json.headline = headline;
|
|
157
|
+
if (image_url)
|
|
158
|
+
json.image_url = image_url;
|
|
159
|
+
if (summary)
|
|
160
|
+
json.summary = summary;
|
|
161
|
+
const resp = await request("POST", "/api/articles/stub", {
|
|
162
|
+
auth: true,
|
|
163
|
+
json,
|
|
164
|
+
});
|
|
165
|
+
const data = (await resp.json());
|
|
166
|
+
const statusMsg = data.status === "created"
|
|
167
|
+
? "Stub created."
|
|
168
|
+
: data.status === "stub_exists"
|
|
169
|
+
? "Stub already exists."
|
|
170
|
+
: "Full article already exists.";
|
|
171
|
+
return `${statusMsg} Slug: ${slug}\nUse [[${slug}|${title}]] to link to this entity.\n\n${JSON.stringify(data, null, 2)}`;
|
|
172
|
+
},
|
|
173
|
+
});
|
|
114
174
|
server.addTool({
|
|
115
175
|
name: "pull",
|
|
116
176
|
description: "Download an article from OpenAlmanac to your local working directory (~/.openalmanac/articles/). " +
|
|
@@ -130,7 +190,12 @@ export function registerArticleTools(server) {
|
|
|
130
190
|
const { frontmatter, content } = parseFrontmatter(markdown);
|
|
131
191
|
const title = frontmatter.title || "(untitled)";
|
|
132
192
|
const wordCount = content.trim().split(/\s+/).filter(Boolean).length;
|
|
133
|
-
|
|
193
|
+
const isStub = frontmatter.stub === true;
|
|
194
|
+
const stubNote = isStub
|
|
195
|
+
? "\n\nThis is a STUB article — a placeholder that hasn't been fully written yet. " +
|
|
196
|
+
"Fill in the content body with a complete article, then push to publish."
|
|
197
|
+
: "";
|
|
198
|
+
return `Pulled "${title}" to ${filePath}\n${wordCount} words, ${frontmatter.sources?.length ?? 0} sources.${stubNote}\n\n${WRITING_GUIDE}`;
|
|
134
199
|
},
|
|
135
200
|
});
|
|
136
201
|
server.addTool({
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { request } from "../auth.js";
|
|
3
|
+
export function registerPeopleTools(server) {
|
|
4
|
+
server.addTool({
|
|
5
|
+
name: "search_people",
|
|
6
|
+
description: "Search for people to find their canonical slug for linking. Returns candidates with name, headline, " +
|
|
7
|
+
"image, and location. Use the returned slug when creating stubs and [[links]] for people. Requires login.",
|
|
8
|
+
parameters: z.object({
|
|
9
|
+
query: z.string().describe("Search terms (e.g. 'John Smith MIT professor')"),
|
|
10
|
+
limit: z.number().min(1).max(10).default(5).describe("Max results (1-10, default 5)"),
|
|
11
|
+
}),
|
|
12
|
+
async execute({ query, limit }) {
|
|
13
|
+
const resp = await request("GET", "/api/people/search", {
|
|
14
|
+
auth: true,
|
|
15
|
+
params: { query, limit },
|
|
16
|
+
});
|
|
17
|
+
return JSON.stringify(await resp.json(), null, 2);
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|