openalmanac 0.2.6 → 0.2.8

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/auth.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- declare const API_BASE = "https://api.openalmanac.org";
1
+ declare const API_BASE: string;
2
2
  declare const API_KEY_PATH: string;
3
3
  declare const ARTICLES_DIR: string;
4
4
  export { API_BASE, API_KEY_PATH, ARTICLES_DIR };
package/dist/auth.js CHANGED
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync } from "
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { chmodSync } from "node:fs";
5
- const API_BASE = "https://api.openalmanac.org";
5
+ const API_BASE = process.env.OPENALMANAC_API_BASE || "https://openalmanac.org/api/proxy";
6
6
  const API_KEY_DIR = join(homedir(), ".openalmanac");
7
7
  const API_KEY_PATH = join(API_KEY_DIR, "api_key");
8
8
  const ARTICLES_DIR = join(homedir(), ".openalmanac", "articles");
package/dist/server.js CHANGED
@@ -2,7 +2,15 @@ import { FastMCP } from "fastmcp";
2
2
  import { registerAuthTools } from "./tools/auth.js";
3
3
  import { registerArticleTools } from "./tools/articles.js";
4
4
  import { registerResearchTools } from "./tools/research.js";
5
+ import { registerCommunityTools } from "./tools/communities.js";
6
+ import { getApiKey } from "./auth.js";
7
+ import { openBrowser } from "./browser.js";
8
+ const NEXT_STEPS_URL = "https://www.openalmanac.org/contribute/next-steps";
5
9
  export function createServer() {
10
+ // On first run (no API key), open the next-steps page so the user knows what to do
11
+ if (!getApiKey()) {
12
+ openBrowser(NEXT_STEPS_URL);
13
+ }
6
14
  const server = new FastMCP({
7
15
  name: "OpenAlmanac",
8
16
  version: "0.1.0",
@@ -18,11 +26,16 @@ export function createServer() {
18
26
  "formulaic conclusions, etc.). Every sentence should contain a specific fact the reader didn't know.\n\n" +
19
27
  "After creating an article, always share the exact URL from the push response with the user. " +
20
28
  "This URL includes a celebration page for the newly created article.\n\n" +
29
+ "After pushing an article, call search_communities to find relevant communities. " +
30
+ "Analyze the article content against community names and descriptions, then suggest " +
31
+ "communities to the user. If the user confirms or skips, call link_article with the " +
32
+ "suggested community slugs. If the user picks specific ones, use those instead.\n\n" +
21
33
  "When working with tool results, write down any important information you might need later " +
22
34
  "in your response, as the original tool result may be cleared later.",
23
35
  });
24
36
  registerAuthTools(server);
25
37
  registerArticleTools(server);
26
38
  registerResearchTools(server);
39
+ registerCommunityTools(server);
27
40
  return server;
28
41
  }
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { stringify as yamlStringify } from "yaml";
5
5
  import { request, ARTICLES_DIR, getAuthStatus } from "../auth.js";
6
6
  import { validateArticle, parseFrontmatter } from "../validate.js";
7
+ import { openBrowser } from "../browser.js";
7
8
  const SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
8
9
  const WRITING_GUIDE = `
9
10
  ## Article structure
@@ -196,6 +197,7 @@ export function registerArticleTools(server) {
196
197
  });
197
198
  const data = (await resp.json());
198
199
  const articleUrl = `https://www.openalmanac.org/article/${slug}?celebrate=true`;
200
+ openBrowser(articleUrl);
199
201
  return `Pushed successfully.\n\nArticle URL (share this exact link with the user): ${articleUrl}\n\n${JSON.stringify(data, null, 2)}`;
200
202
  },
201
203
  });
@@ -0,0 +1,2 @@
1
+ import { FastMCP } from "fastmcp";
2
+ export declare function registerCommunityTools(server: FastMCP): void;
@@ -0,0 +1,127 @@
1
+ import { z } from "zod";
2
+ import { request } from "../auth.js";
3
+ export function registerCommunityTools(server) {
4
+ server.addTool({
5
+ name: "search_communities",
6
+ description: "Search or list OpenAlmanac communities. Returns community names, descriptions, and member counts. " +
7
+ "Use this after pushing an article to find relevant communities for auto-linking. No authentication needed.",
8
+ parameters: z.object({
9
+ query: z
10
+ .string()
11
+ .optional()
12
+ .describe("Search term (case-insensitive match on name, slug, or description). Omit to list all."),
13
+ sort: z
14
+ .enum(["popular", "newest"])
15
+ .default("popular")
16
+ .describe("Sort order (default: popular)"),
17
+ limit: z
18
+ .number()
19
+ .min(1)
20
+ .max(100)
21
+ .default(20)
22
+ .describe("Max results (1-100, default 20)"),
23
+ }),
24
+ async execute({ query, sort, limit }) {
25
+ const params = { sort, limit };
26
+ if (query)
27
+ params.query = query;
28
+ const resp = await request("GET", "/api/communities", { params });
29
+ const data = (await resp.json());
30
+ const communities = data.communities.map((c) => ({
31
+ slug: c.slug,
32
+ name: c.name,
33
+ description: c.description,
34
+ member_count: c.member_count,
35
+ created_at: c.created_at,
36
+ }));
37
+ return `Found ${data.total} communities:\n\n${JSON.stringify(communities, null, 2)}`;
38
+ },
39
+ });
40
+ server.addTool({
41
+ name: "create_community",
42
+ description: "Create a new OpenAlmanac community. Requires login and at least 1 published article. " +
43
+ "Communities are spaces where articles can be curated and discussed around a topic.",
44
+ parameters: z.object({
45
+ name: z.string().min(1).max(100).describe("Community name (1-100 chars)"),
46
+ slug: z
47
+ .string()
48
+ .min(1)
49
+ .max(100)
50
+ .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)
51
+ .describe("Unique kebab-case identifier (e.g. 'machine-learning')"),
52
+ description: z
53
+ .string()
54
+ .min(1)
55
+ .max(2000)
56
+ .describe("What the community is about (1-2000 chars)"),
57
+ }),
58
+ async execute({ name, slug, description }) {
59
+ const resp = await request("POST", "/api/communities", {
60
+ auth: true,
61
+ json: { name, slug, description },
62
+ });
63
+ const data = (await resp.json());
64
+ const communityUrl = `https://www.openalmanac.org/communities/${slug}`;
65
+ return `Community created!\n\nURL: ${communityUrl}\n\n${JSON.stringify(data, null, 2)}`;
66
+ },
67
+ });
68
+ server.addTool({
69
+ name: "create_post",
70
+ description: "Create a post in an OpenAlmanac community. Requires login and community membership. " +
71
+ "If you get a 403 error, you need to join the community first.",
72
+ parameters: z.object({
73
+ community_slug: z.string().describe("Community slug (e.g. 'machine-learning')"),
74
+ title: z.string().min(1).max(300).describe("Post title (1-300 chars)"),
75
+ body: z.string().max(10000).default("").describe("Post body (max 10000 chars)"),
76
+ flair: z
77
+ .enum(["discussion", "article-request", "question", "announcement"])
78
+ .optional()
79
+ .describe("Post flair/category"),
80
+ }),
81
+ async execute({ community_slug, title, body, flair }) {
82
+ const json = { title, body };
83
+ if (flair)
84
+ json.flair = flair;
85
+ const resp = await request("POST", `/api/communities/${community_slug}/posts`, {
86
+ auth: true,
87
+ json,
88
+ });
89
+ const data = (await resp.json());
90
+ const postUrl = `https://www.openalmanac.org/communities/${community_slug}/post/${data.id}`;
91
+ return `Post created!\n\nURL: ${postUrl}\n\n${JSON.stringify(data, null, 2)}`;
92
+ },
93
+ });
94
+ server.addTool({
95
+ name: "link_article",
96
+ description: "Link an article to one or more communities. Use this after pushing an article to connect it " +
97
+ "with relevant communities. Call search_communities first to find matching communities. " +
98
+ "Idempotent — already-linked articles are reported but don't cause errors. Requires login.",
99
+ parameters: z.object({
100
+ article_id: z.string().describe("Article slug/ID to link (e.g. 'machine-learning')"),
101
+ community_slugs: z
102
+ .array(z.string())
103
+ .min(1)
104
+ .max(50)
105
+ .describe("List of community slugs to link the article to (max 50)"),
106
+ }),
107
+ async execute({ article_id, community_slugs }) {
108
+ const resp = await request("POST", `/api/articles/${article_id}/auto-link`, {
109
+ auth: true,
110
+ json: { community_slugs },
111
+ });
112
+ const data = (await resp.json());
113
+ const lines = [];
114
+ if (data.linked.length > 0) {
115
+ lines.push(`Linked to ${data.linked.length} communities: ${data.linked.join(", ")}`);
116
+ }
117
+ if (data.failed.length > 0) {
118
+ const failLines = data.failed.map((f) => ` ${f.slug}: ${f.reason}`);
119
+ lines.push(`Failed (${data.failed.length}):\n${failLines.join("\n")}`);
120
+ }
121
+ if (lines.length === 0) {
122
+ lines.push("No communities to link.");
123
+ }
124
+ return lines.join("\n\n");
125
+ },
126
+ });
127
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openalmanac",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "OpenAlmanac — pull, edit, and push articles to the open knowledge base",
5
5
  "type": "module",
6
6
  "bin": {