openalmanac 0.2.5 → 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 +8 -1
- package/dist/auth.js +20 -1
- package/dist/server.js +13 -0
- package/dist/tools/articles.js +33 -20
- package/dist/tools/communities.d.ts +2 -0
- package/dist/tools/communities.js +127 -0
- package/package.json +1 -1
package/dist/auth.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
declare const API_BASE
|
|
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 };
|
|
@@ -6,6 +6,13 @@ export declare function getApiKey(): string | null;
|
|
|
6
6
|
export declare function requireApiKey(): string;
|
|
7
7
|
export declare function saveApiKey(key: string): void;
|
|
8
8
|
export declare function removeApiKey(): boolean;
|
|
9
|
+
export type AuthStatus = {
|
|
10
|
+
loggedIn: true;
|
|
11
|
+
name: string;
|
|
12
|
+
} | {
|
|
13
|
+
loggedIn: false;
|
|
14
|
+
};
|
|
15
|
+
export declare function getAuthStatus(): Promise<AuthStatus>;
|
|
9
16
|
export declare function buildAuthHeaders(): Record<string, string>;
|
|
10
17
|
export declare function request(method: string, path: string, options?: {
|
|
11
18
|
auth?: boolean;
|
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://
|
|
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");
|
|
@@ -37,6 +37,25 @@ export function removeApiKey() {
|
|
|
37
37
|
}
|
|
38
38
|
return false;
|
|
39
39
|
}
|
|
40
|
+
export async function getAuthStatus() {
|
|
41
|
+
const key = getApiKey();
|
|
42
|
+
if (!key)
|
|
43
|
+
return { loggedIn: false };
|
|
44
|
+
try {
|
|
45
|
+
const resp = await fetch(`${API_BASE}/api/agents/me`, {
|
|
46
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
47
|
+
signal: AbortSignal.timeout(10_000),
|
|
48
|
+
});
|
|
49
|
+
if (resp.ok) {
|
|
50
|
+
const data = (await resp.json());
|
|
51
|
+
return { loggedIn: true, name: data.name ?? "unknown" };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Key invalid or network error
|
|
56
|
+
}
|
|
57
|
+
return { loggedIn: false };
|
|
58
|
+
}
|
|
40
59
|
export function buildAuthHeaders() {
|
|
41
60
|
return { Authorization: `Bearer ${requireApiKey()}` };
|
|
42
61
|
}
|
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
|
}
|
package/dist/tools/articles.js
CHANGED
|
@@ -2,8 +2,9 @@ import { z } from "zod";
|
|
|
2
2
|
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { stringify as yamlStringify } from "yaml";
|
|
5
|
-
import { request, ARTICLES_DIR } from "../auth.js";
|
|
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
|
|
@@ -15,6 +16,7 @@ title: Article Title
|
|
|
15
16
|
sources:
|
|
16
17
|
- url: https://example.com
|
|
17
18
|
title: Source Title
|
|
19
|
+
accessed_date: "2025-01-15"
|
|
18
20
|
infobox:
|
|
19
21
|
header:
|
|
20
22
|
image_url: https://... # optional hero image
|
|
@@ -25,34 +27,40 @@ infobox:
|
|
|
25
27
|
- key: Occupation
|
|
26
28
|
value: Scientist
|
|
27
29
|
links:
|
|
28
|
-
-
|
|
29
|
-
url: https://...
|
|
30
|
+
- https://example.com
|
|
30
31
|
sections:
|
|
31
32
|
- type: timeline # chronological events
|
|
32
33
|
title: Career Timeline
|
|
33
34
|
items:
|
|
34
|
-
- primary: "
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
- primary: "Started company"
|
|
36
|
+
period: "2010"
|
|
37
|
+
location: "San Francisco"
|
|
38
|
+
- type: list # key figures, works, features
|
|
37
39
|
title: Known For
|
|
38
40
|
items:
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
+
- title: First achievement
|
|
42
|
+
- title: Second achievement
|
|
43
|
+
subtitle: Additional detail
|
|
41
44
|
- type: tags # inline tags/chips
|
|
42
45
|
title: Genres
|
|
43
46
|
items:
|
|
44
|
-
-
|
|
45
|
-
-
|
|
47
|
+
- Rock
|
|
48
|
+
- Jazz
|
|
46
49
|
- type: grid # image grid
|
|
47
50
|
title: Gallery
|
|
48
51
|
items:
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
- type: table #
|
|
52
|
+
- title: Caption
|
|
53
|
+
image_url: https://...
|
|
54
|
+
- type: table # structured comparison
|
|
52
55
|
title: Statistics
|
|
53
56
|
items:
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
headers:
|
|
58
|
+
- Name
|
|
59
|
+
- Value
|
|
60
|
+
rows:
|
|
61
|
+
- cells:
|
|
62
|
+
- Height
|
|
63
|
+
- "6'2\\""
|
|
56
64
|
- type: key_value # simple key-value pairs
|
|
57
65
|
title: Quick Facts
|
|
58
66
|
items:
|
|
@@ -189,18 +197,23 @@ export function registerArticleTools(server) {
|
|
|
189
197
|
});
|
|
190
198
|
const data = (await resp.json());
|
|
191
199
|
const articleUrl = `https://www.openalmanac.org/article/${slug}?celebrate=true`;
|
|
192
|
-
|
|
200
|
+
openBrowser(articleUrl);
|
|
201
|
+
return `Pushed successfully.\n\nArticle URL (share this exact link with the user): ${articleUrl}\n\n${JSON.stringify(data, null, 2)}`;
|
|
193
202
|
},
|
|
194
203
|
});
|
|
195
204
|
server.addTool({
|
|
196
205
|
name: "status",
|
|
197
|
-
description: "
|
|
198
|
-
"Shows filename, title, file size, and last modified time.
|
|
206
|
+
description: "Show login status and list all article files in your local working directory (~/.openalmanac/articles/). " +
|
|
207
|
+
"Shows auth state, filename, title, file size, and last modified time.",
|
|
199
208
|
async execute() {
|
|
200
209
|
ensureArticlesDir();
|
|
210
|
+
const auth = await getAuthStatus();
|
|
211
|
+
const authLine = auth.loggedIn
|
|
212
|
+
? `Logged in as ${auth.name}.`
|
|
213
|
+
: "Not logged in. Use login to authenticate.";
|
|
201
214
|
const files = readdirSync(ARTICLES_DIR).filter((f) => f.endsWith(".md"));
|
|
202
215
|
if (files.length === 0) {
|
|
203
|
-
return
|
|
216
|
+
return `${authLine}\n\nLocal articles (0 files in ${ARTICLES_DIR}):\n (none — use pull or new to get started)`;
|
|
204
217
|
}
|
|
205
218
|
const rows = [];
|
|
206
219
|
for (const file of files) {
|
|
@@ -215,7 +228,7 @@ export function registerArticleTools(server) {
|
|
|
215
228
|
const modified = stat.mtime.toISOString().slice(0, 16).replace("T", " ");
|
|
216
229
|
rows.push(` ${file} — "${title}" (${size}, modified ${modified})`);
|
|
217
230
|
}
|
|
218
|
-
return `${files.length}
|
|
231
|
+
return `${authLine}\n\nLocal articles (${files.length} file(s) in ${ARTICLES_DIR}):\n${rows.join("\n")}`;
|
|
219
232
|
},
|
|
220
233
|
});
|
|
221
234
|
}
|
|
@@ -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
|
+
}
|