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 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 [N] citation markers mapped to sources.",
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 the user agrees to write an article:",
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. **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.",
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. **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.",
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. **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.",
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
- " Write the full article body with citation markers [N]. 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.",
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
- "5. **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.",
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
- "6. **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.",
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
- "7. **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.",
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 ? `added to ${DIM}~/.claude.json${RST}` : `${DIM}already configured${RST}`}`);
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 ? `added to ${DIM}~/.claude.json${RST}` : `${DIM}already configured${RST}`}`);
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 ? `added to ${DIM}~/.claude.json${RST}` : `${DIM}already configured${RST}`}`);
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);
@@ -14,7 +14,8 @@ const WRITING_GUIDE = `
14
14
  article_id: the-slug
15
15
  title: Article Title
16
16
  sources:
17
- - url: https://example.com
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 [1] citation markers...
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 [N] after punctuation: "The population is 1.4 billion.[1]"
81
- - Number sequentially starting at [1]
82
- - Every source in the sources list must be referenced at least once in the body
83
- - Every [N] marker must have a matching source
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 MARKER_RE = /\[(\d+)\]/g;
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
- // citation markers
66
- const markerNums = new Set();
67
- let match;
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
- for (let i = 1; i < sorted.length; i++) {
77
- if (sorted[i] !== sorted[i - 1] + 1) {
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: `Gap in citation markers: [${sorted[i - 1]}] [${sorted[i]}]`,
93
+ message: `[@${key}] referenced in content but no source has key "${key}"`,
81
94
  });
82
- break;
83
95
  }
84
96
  }
85
- // cross-check markers vs sources
86
- if (Array.isArray(sources)) {
87
- const maxMarker = sorted[sorted.length - 1];
88
- if (maxMarker > sources.length) {
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: "citations",
91
- message: `Citation [${maxMarker}] referenced but only ${sources.length} source(s) provided`,
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openalmanac",
3
- "version": "0.2.28",
3
+ "version": "0.2.30",
4
4
  "description": "OpenAlmanac — pull, edit, and push articles to the open knowledge base",
5
5
  "type": "module",
6
6
  "bin": {