openalmanac 0.2.11 → 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 CHANGED
@@ -3,6 +3,7 @@ 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
8
  export function createServer() {
8
9
  if (!getApiKey()) {
@@ -34,6 +35,16 @@ export function createServer() {
34
35
  "Before writing or editing any article, read https://www.openalmanac.org/ai-patterns-to-avoid.md " +
35
36
  "— it covers AI writing patterns that erode trust (inflated significance, promotional language, " +
36
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" +
37
48
  "After creating an article, always share the exact URL from the push response with the user. " +
38
49
  "This URL includes a celebration page for the newly created article.\n\n" +
39
50
  "After pushing an article, call search_communities to find relevant communities. " +
@@ -47,5 +58,6 @@ export function createServer() {
47
58
  registerArticleTools(server);
48
59
  registerResearchTools(server);
49
60
  registerCommunityTools(server);
61
+ registerPeopleTools(server);
50
62
  return server;
51
63
  }
@@ -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. No authentication needed.",
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 resp = await request("GET", "/api/search", {
109
- params: { query, limit, page },
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
- return `Pulled "${title}" to ${filePath}\n${wordCount} words, ${frontmatter.sources?.length ?? 0} sources.\n\n${WRITING_GUIDE}`;
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,2 @@
1
+ import { FastMCP } from "fastmcp";
2
+ export declare function registerPeopleTools(server: FastMCP): void;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openalmanac",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "OpenAlmanac — pull, edit, and push articles to the open knowledge base",
5
5
  "type": "module",
6
6
  "bin": {