openalmanac 0.2.28 → 0.2.30
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 +11 -9
- package/dist/setup.js +5 -5
- package/dist/tools/articles.js +40 -6
- package/dist/validate.js +35 -30
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -31,7 +31,7 @@ export function createServer() {
|
|
|
31
31
|
name: "OpenAlmanac",
|
|
32
32
|
version: pkg.version,
|
|
33
33
|
instructions: [
|
|
34
|
-
"OpenAlmanac is an open knowledge base — a Wikipedia anyone can read from and write to through an API. Articles are markdown files with YAML frontmatter and [
|
|
34
|
+
"OpenAlmanac is an open knowledge base — a Wikipedia anyone can read from and write to through an API. Articles are markdown files with YAML frontmatter and [@key] citation markers mapped to named sources.",
|
|
35
35
|
"",
|
|
36
36
|
"## How this should feel",
|
|
37
37
|
"",
|
|
@@ -95,28 +95,30 @@ export function createServer() {
|
|
|
95
95
|
"",
|
|
96
96
|
"## Writing flow",
|
|
97
97
|
"",
|
|
98
|
-
"When
|
|
98
|
+
"When you've researched enough and a specific article topic has come into focus:",
|
|
99
99
|
"",
|
|
100
100
|
'1. **Align briefly with the user** — Talk about what the article should cover, what to focus on, what angle to take. Not a rigid outline — a quick conversation. "I\'m thinking we cover the history, the Royal Brahmins, daily worship, and the Ramakien — anything you want to add or skip?"',
|
|
101
101
|
"",
|
|
102
|
-
"2. **
|
|
102
|
+
"2. **Propose the article** — Call `propose_article` with your summary (title, key sections, angle — 3-5 bullet points) and a detailed brief (all sources, key facts, user preferences, angle, what to avoid, related articles). Do not start writing an article without proposing first. The client environment determines what happens next — in some environments the user will see a plan card with options (write here vs. run in background), in others you'll get a text response telling you to proceed. Follow whatever the tool response says.",
|
|
103
103
|
"",
|
|
104
|
-
"3. **
|
|
104
|
+
"3. **Read the writing guidelines** — Fetch https://www.openalmanac.org/writing-guidelines.md and https://www.openalmanac.org/ai-patterns-to-avoid.md before writing a single word.",
|
|
105
105
|
"",
|
|
106
|
-
"4. **
|
|
106
|
+
"4. **Scaffold** — Use `new` to create the article file. List ALL sources you've gathered in the frontmatter before writing any body text. Don't discard sources — if you read it during research and it's relevant, include it.",
|
|
107
107
|
"",
|
|
108
|
-
"
|
|
108
|
+
"5. **Write a pure text draft** — This whole process (writing, review, fact-check, images, linking) takes a few minutes. Let the user know in a fun way that they can step away — and that once it's ready, you're happy to discuss any edits or polishing.",
|
|
109
109
|
"",
|
|
110
|
-
"
|
|
110
|
+
" Write the full article body with citation markers [@key]. No wikilinks, no `[[slug|Display Text]]` syntax, no images, no stubs. Just prose and citations. The linking and images come later from subagents who need to read the finished text.",
|
|
111
|
+
"",
|
|
112
|
+
"6. **Dispatch four subagents in parallel** — After the draft is complete, dispatch these simultaneously. Each agent has its own guidelines file — tell it to fetch and read that file as its first step. The guidelines file tells the agent what to do, what additional guidelines to fetch, and what format to return results in.",
|
|
111
113
|
"",
|
|
112
114
|
" - **Review agent** → tell it to read https://www.openalmanac.org/review-guidelines.md and review the draft at `~/.openalmanac/articles/{slug}.md`",
|
|
113
115
|
" - **Fact-check agent** → tell it to read https://www.openalmanac.org/fact-checking-guidelines.md and fact-check the draft at `~/.openalmanac/articles/{slug}.md`",
|
|
114
116
|
" - **Image agent** → tell it to read https://www.openalmanac.org/image-guidelines.md and find images for the draft at `~/.openalmanac/articles/{slug}.md`",
|
|
115
117
|
" - **Linking agent** → tell it to read https://www.openalmanac.org/linking-guidelines.md and create stubs/wikilinks for the draft at `~/.openalmanac/articles/{slug}.md`",
|
|
116
118
|
"",
|
|
117
|
-
"
|
|
119
|
+
"7. **Integrate** — Present the review and fact-check feedback to the user. Then fix everything in one pass: review issues, fact-check corrections, add images, add wikilinks.",
|
|
118
120
|
"",
|
|
119
|
-
"
|
|
121
|
+
"8. **Publish** — Validate and publish. Share the exact URL from the publish response (it includes a celebration page). Then search_communities for relevant communities and suggest linking.",
|
|
120
122
|
"",
|
|
121
123
|
"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.",
|
|
122
124
|
"",
|
package/dist/setup.js
CHANGED
|
@@ -159,7 +159,7 @@ function configureMcp() {
|
|
|
159
159
|
if (!desktop.mcpServers)
|
|
160
160
|
desktop.mcpServers = {};
|
|
161
161
|
if (!isAlmanacCurrent(desktop.mcpServers.almanac)) {
|
|
162
|
-
desktop.mcpServers.almanac = ALMANAC_MCP_ENTRY;
|
|
162
|
+
desktop.mcpServers.almanac = { ...ALMANAC_MCP_ENTRY };
|
|
163
163
|
writeJson(CLAUDE_JSON, desktop);
|
|
164
164
|
changed = true;
|
|
165
165
|
}
|
|
@@ -170,7 +170,7 @@ function configureMcp() {
|
|
|
170
170
|
if (!code.mcpServers)
|
|
171
171
|
code.mcpServers = {};
|
|
172
172
|
if (!isAlmanacCurrent(code.mcpServers.almanac)) {
|
|
173
|
-
code.mcpServers.almanac = ALMANAC_MCP_ENTRY;
|
|
173
|
+
code.mcpServers.almanac = { ...ALMANAC_MCP_ENTRY };
|
|
174
174
|
writeJson(CLAUDE_CODE_MCP, code);
|
|
175
175
|
changed = true;
|
|
176
176
|
}
|
|
@@ -277,7 +277,7 @@ async function runLoginStep(agent, mcpChanged, toolCount) {
|
|
|
277
277
|
const priorSteps = () => {
|
|
278
278
|
stepDone(`Agent \u2192 ${WHITE_BOLD}${agent}${RST}`);
|
|
279
279
|
w(BAR);
|
|
280
|
-
stepDone(`MCP server ${mcpChanged ?
|
|
280
|
+
stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
|
|
281
281
|
w(BAR);
|
|
282
282
|
stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed`);
|
|
283
283
|
w(BAR);
|
|
@@ -431,7 +431,7 @@ function renderToolSelect(selected, cursor, agent, mcpChanged) {
|
|
|
431
431
|
w("");
|
|
432
432
|
stepDone(`Agent \u2192 ${WHITE_BOLD}${agent}${RST}`);
|
|
433
433
|
w(BAR);
|
|
434
|
-
stepDone(`MCP server ${mcpChanged ?
|
|
434
|
+
stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
|
|
435
435
|
w(BAR);
|
|
436
436
|
stepActive(`Select tool permissions ${DIM}deselect any you'd rather approve manually${RST}`);
|
|
437
437
|
w(BAR);
|
|
@@ -500,7 +500,7 @@ function printResult(agent, loginResult, mcpChanged, toolCount) {
|
|
|
500
500
|
w("");
|
|
501
501
|
stepDone(`Agent \u2192 ${WHITE_BOLD}${agent}${RST}`);
|
|
502
502
|
w(BAR);
|
|
503
|
-
stepDone(`MCP server ${mcpChanged ?
|
|
503
|
+
stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
|
|
504
504
|
w(BAR);
|
|
505
505
|
stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed in ${DIM}~/.claude/settings.json${RST}`);
|
|
506
506
|
w(BAR);
|
package/dist/tools/articles.js
CHANGED
|
@@ -14,7 +14,8 @@ const WRITING_GUIDE = `
|
|
|
14
14
|
article_id: the-slug
|
|
15
15
|
title: Article Title
|
|
16
16
|
sources:
|
|
17
|
-
-
|
|
17
|
+
- key: example-source-title
|
|
18
|
+
url: https://example.com
|
|
18
19
|
title: Source Title
|
|
19
20
|
accessed_date: "2025-01-15"
|
|
20
21
|
infobox:
|
|
@@ -68,7 +69,7 @@ infobox:
|
|
|
68
69
|
value: "1.4 billion"
|
|
69
70
|
---
|
|
70
71
|
|
|
71
|
-
Article body with [
|
|
72
|
+
Article body with [@key] citation markers...
|
|
72
73
|
\`\`\`
|
|
73
74
|
|
|
74
75
|
## Infobox
|
|
@@ -77,10 +78,12 @@ Include an infobox for any article about a person, place, organization, event, o
|
|
|
77
78
|
|
|
78
79
|
## Citations
|
|
79
80
|
|
|
80
|
-
- Mark claims with [
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
- Every
|
|
81
|
+
- Mark claims with [@key] after punctuation: "The population is 1.4 billion.[@who-world-population]"
|
|
82
|
+
- Keys must be kebab-case with at least one hyphen (e.g. 'nytimes-climate-report', 'who-malaria-2024')
|
|
83
|
+
- Generate keys BibTeX-style: {domain}-{title-words} (e.g. 'arxiv-attention-is-all')
|
|
84
|
+
- Every source in the sources list must be referenced at least once in the body with [@key]
|
|
85
|
+
- Every [@key] marker must have a matching source with that key
|
|
86
|
+
- Display numbers are computed automatically from first-appearance order — just use the keys
|
|
84
87
|
|
|
85
88
|
## Images
|
|
86
89
|
|
|
@@ -308,6 +311,37 @@ export function registerArticleTools(server) {
|
|
|
308
311
|
return JSON.stringify(await resp.json(), null, 2);
|
|
309
312
|
},
|
|
310
313
|
});
|
|
314
|
+
server.addTool({
|
|
315
|
+
name: "propose_article",
|
|
316
|
+
description: "Propose an article before writing it. Call this when you've researched enough and a specific article topic has come into focus. " +
|
|
317
|
+
"Structures your proposal with a user-facing summary and a detailed brief. " +
|
|
318
|
+
"The client environment determines what happens next — in GUI environments the user sees a plan card with options, " +
|
|
319
|
+
"in CLI environments you'll get a response telling you to proceed with writing. " +
|
|
320
|
+
"Do not start writing an article without proposing first.",
|
|
321
|
+
parameters: z.object({
|
|
322
|
+
summary: z.string().describe("User-facing summary: title, key sections, angle. Markdown. Concise — 3-5 bullet points."),
|
|
323
|
+
details: z.string().describe("Full handoff brief for the background agent. Include: all sources, key facts, user preferences, angle, what to avoid, related articles. Be thorough."),
|
|
324
|
+
title: z.string().describe("Proposed article title"),
|
|
325
|
+
slug: z.string().describe("Proposed article slug (kebab-case)"),
|
|
326
|
+
_userChoice: z.enum(["background", "here", "expired", "already_in_progress"]).optional().describe("Internal field set by GUI client. Never set this manually."),
|
|
327
|
+
}),
|
|
328
|
+
async execute({ summary, details, title, slug, _userChoice }) {
|
|
329
|
+
if (!SLUG_RE.test(slug)) {
|
|
330
|
+
throw new Error(`Invalid slug "${slug}". Must be kebab-case (e.g. 'machine-learning'). Pattern: ^[a-z0-9]+(-[a-z0-9]+)*$`);
|
|
331
|
+
}
|
|
332
|
+
if (_userChoice === "background") {
|
|
333
|
+
return `Article "${title}" is now being written in a background process. Continue exploring with the user. Do not write this article in this conversation.`;
|
|
334
|
+
}
|
|
335
|
+
if (_userChoice === "expired") {
|
|
336
|
+
return `The user navigated away before responding to the proposal. Proposal expired. Continue the conversation naturally.`;
|
|
337
|
+
}
|
|
338
|
+
if (_userChoice === "already_in_progress") {
|
|
339
|
+
return `Article "${title}" is already being generated in a background process. No action needed.`;
|
|
340
|
+
}
|
|
341
|
+
// "here" OR no _userChoice (CLI default) — proceed with writing
|
|
342
|
+
return `Article Proposal: ${title}\n\n${summary}\n\nProceed with writing this article following the writing flow in your instructions.`;
|
|
343
|
+
},
|
|
344
|
+
});
|
|
311
345
|
server.addTool({
|
|
312
346
|
name: "status",
|
|
313
347
|
description: "Show login status and list all article files in your local working directory (~/.openalmanac/articles/). " +
|
package/dist/validate.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { parse as parseYaml } from "yaml";
|
|
2
2
|
const SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
3
3
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
4
|
-
const
|
|
4
|
+
const KEY_RE = /^[a-z0-9]+-[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
5
|
+
const CITE_RE = /\[@([a-z0-9]+-[a-z0-9]+(?:-[a-z0-9]+)*)\]/g;
|
|
5
6
|
export function parseFrontmatter(raw) {
|
|
6
7
|
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
7
8
|
if (!match) {
|
|
@@ -39,8 +40,26 @@ export function validateArticle(raw) {
|
|
|
39
40
|
errors.push({ field: "sources", message: "Sources must be an array" });
|
|
40
41
|
}
|
|
41
42
|
else {
|
|
43
|
+
const seenKeys = new Set();
|
|
42
44
|
for (let i = 0; i < sources.length; i++) {
|
|
43
45
|
const s = sources[i];
|
|
46
|
+
// key validation
|
|
47
|
+
const key = s.key;
|
|
48
|
+
if (!key || typeof key !== "string") {
|
|
49
|
+
errors.push({ field: `sources[${i}].key`, message: "Key is required" });
|
|
50
|
+
}
|
|
51
|
+
else if (!KEY_RE.test(key)) {
|
|
52
|
+
errors.push({
|
|
53
|
+
field: `sources[${i}].key`,
|
|
54
|
+
message: `Key "${key}" must be kebab-case with at least one hyphen (e.g. 'nytimes-climate-report')`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else if (seenKeys.has(key)) {
|
|
58
|
+
errors.push({ field: `sources[${i}].key`, message: `Duplicate key "${key}"` });
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
seenKeys.add(key);
|
|
62
|
+
}
|
|
44
63
|
if (!s.url || typeof s.url !== "string") {
|
|
45
64
|
errors.push({ field: `sources[${i}].url`, message: "URL is required" });
|
|
46
65
|
}
|
|
@@ -61,44 +80,30 @@ export function validateArticle(raw) {
|
|
|
61
80
|
errors.push({ field: `sources[${i}].accessed_date`, message: "Must be YYYY-MM-DD format" });
|
|
62
81
|
}
|
|
63
82
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
while ((match = MARKER_RE.exec(content)) !== null) {
|
|
69
|
-
markerNums.add(parseInt(match[1], 10));
|
|
70
|
-
}
|
|
71
|
-
if (markerNums.size > 0) {
|
|
72
|
-
const sorted = [...markerNums].sort((a, b) => a - b);
|
|
73
|
-
if (sorted[0] !== 1) {
|
|
74
|
-
errors.push({ field: "citations", message: "Citation markers must start at [1]" });
|
|
83
|
+
// citation markers — collect all [@key] references from content
|
|
84
|
+
const citedKeys = new Set();
|
|
85
|
+
for (const match of content.matchAll(CITE_RE)) {
|
|
86
|
+
citedKeys.add(match[1]);
|
|
75
87
|
}
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
// cross-check: every [@key] in body must have a matching source
|
|
89
|
+
for (const key of citedKeys) {
|
|
90
|
+
if (!seenKeys.has(key)) {
|
|
78
91
|
errors.push({
|
|
79
92
|
field: "citations",
|
|
80
|
-
message: `
|
|
93
|
+
message: `[@${key}] referenced in content but no source has key "${key}"`,
|
|
81
94
|
});
|
|
82
|
-
break;
|
|
83
95
|
}
|
|
84
96
|
}
|
|
85
|
-
// cross-check
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
97
|
+
// cross-check: every source key must be referenced at least once
|
|
98
|
+
for (let i = 0; i < sources.length; i++) {
|
|
99
|
+
const s = sources[i];
|
|
100
|
+
const key = s.key;
|
|
101
|
+
if (key && typeof key === "string" && !citedKeys.has(key)) {
|
|
89
102
|
errors.push({
|
|
90
|
-
field:
|
|
91
|
-
message: `
|
|
103
|
+
field: `sources[${i}]`,
|
|
104
|
+
message: `Source "${key}" is never referenced with [@${key}] in the content`,
|
|
92
105
|
});
|
|
93
106
|
}
|
|
94
|
-
for (let i = 0; i < sources.length; i++) {
|
|
95
|
-
if (!markerNums.has(i + 1)) {
|
|
96
|
-
errors.push({
|
|
97
|
-
field: `sources[${i}]`,
|
|
98
|
-
message: `Source ${i + 1} is never referenced with [${i + 1}] in the content`,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
109
|
return errors;
|