openalmanac 0.2.32 → 0.2.33

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.
@@ -6,6 +6,24 @@ import { request, ARTICLES_DIR, getAuthStatus } from "../auth.js";
6
6
  import { validateArticle, parseFrontmatter } from "../validate.js";
7
7
  import { openBrowser } from "../browser.js";
8
8
  const SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
9
+ /**
10
+ * Workaround for Claude Agent SDK MCP transport bug (#18260):
11
+ * Array/object parameters are sometimes serialized as JSON strings
12
+ * instead of native values. This preprocessor coerces them back.
13
+ */
14
+ function coerceJson(schema) {
15
+ return z.preprocess((val) => {
16
+ if (typeof val === "string") {
17
+ try {
18
+ return JSON.parse(val);
19
+ }
20
+ catch {
21
+ return val;
22
+ }
23
+ }
24
+ return val;
25
+ }, schema);
26
+ }
9
27
  const WRITING_GUIDE = `
10
28
  ## Article structure
11
29
 
@@ -131,7 +149,7 @@ export function registerArticleTools(server) {
131
149
  "Use this to check if articles or stubs exist before creating them, or to find entity slugs for wikilinks. " +
132
150
  "Results include 'stub' field (true/false) and 'entity_type' field. No authentication needed.",
133
151
  parameters: z.object({
134
- queries: z.array(z.string()).min(1).max(20).describe("Search queries (1-20)"),
152
+ queries: coerceJson(z.array(z.string()).min(1).max(20)).describe("Search queries (1-20)"),
135
153
  limit: z.number().default(5).describe("Max results per query (1-50, default 5)"),
136
154
  include_stubs: z.boolean().default(true).describe("Include stub articles in results (default true)"),
137
155
  }),
@@ -148,7 +166,7 @@ export function registerArticleTools(server) {
148
166
  "Use this to reference or summarize existing articles in conversation. " +
149
167
  "For editing articles locally, use 'download' instead. No authentication needed.",
150
168
  parameters: z.object({
151
- slugs: z.array(z.string()).min(1).max(20).describe("Article slugs to read (1-20)"),
169
+ slugs: coerceJson(z.array(z.string()).min(1).max(20)).describe("Article slugs to read (1-20)"),
152
170
  }),
153
171
  async execute({ slugs }) {
154
172
  const resp = await request("POST", "/api/articles/batch", {
@@ -163,7 +181,7 @@ export function registerArticleTools(server) {
163
181
  "Use this for every entity (person, organization, topic, etc.) mentioned in an article. " +
164
182
  "Idempotent: existing slugs return their current status. Requires login.",
165
183
  parameters: z.object({
166
- stubs: z.array(z.object({
184
+ stubs: coerceJson(z.array(z.object({
167
185
  slug: z
168
186
  .string()
169
187
  .min(1)
@@ -187,7 +205,7 @@ export function registerArticleTools(server) {
187
205
  .optional()
188
206
  .describe("2-4 sentence summary of the entity. This becomes the stub page content. " +
189
207
  "Be informative — include key facts, dates, and context."),
190
- })).min(1).max(50).describe("Stubs to create (1-50)"),
208
+ })).min(1).max(50)).describe("Stubs to create (1-50)"),
191
209
  }),
192
210
  async execute({ stubs }) {
193
211
  const resp = await request("POST", "/api/articles/stubs", {
@@ -300,7 +318,11 @@ export function registerArticleTools(server) {
300
318
  });
301
319
  const data = (await resp.json());
302
320
  const articleUrl = `https://www.openalmanac.org/article/${slug}?celebrate=true`;
303
- openBrowser(articleUrl);
321
+ const inGui = process.env.OPENALMANAC_GUI === "1";
322
+ // Skip browser open when running inside the GUI — it handles navigation itself
323
+ if (!inGui) {
324
+ openBrowser(articleUrl);
325
+ }
304
326
  // Clean up local files after successful publish
305
327
  let cleanupWarning = "";
306
328
  try {
@@ -319,7 +341,10 @@ export function registerArticleTools(server) {
319
341
  cleanupWarning += `\nNote: could not remove original copy: ${e.message}`;
320
342
  }
321
343
  }
322
- return `Pushed successfully.\n\nArticle URL (share this exact link with the user): ${articleUrl}${cleanupWarning}\n\n${JSON.stringify(data, null, 2)}`;
344
+ const urlLine = inGui
345
+ ? "The article has been published! Let the user know it's live. Do not send them to a web URL."
346
+ : `Article URL (share this exact link with the user): ${articleUrl}`;
347
+ return `Pushed successfully.\n\n${urlLine}${cleanupWarning}\n\n${JSON.stringify(data, null, 2)}`;
323
348
  },
324
349
  });
325
350
  server.addTool({
@@ -1,5 +1,23 @@
1
1
  import { z } from "zod";
2
2
  import { request } from "../auth.js";
3
+ /**
4
+ * Workaround for Claude Agent SDK MCP transport bug (#18260):
5
+ * Array/object parameters are sometimes serialized as JSON strings
6
+ * instead of native values. This preprocessor coerces them back.
7
+ */
8
+ function coerceJson(schema) {
9
+ return z.preprocess((val) => {
10
+ if (typeof val === "string") {
11
+ try {
12
+ return JSON.parse(val);
13
+ }
14
+ catch {
15
+ return val;
16
+ }
17
+ }
18
+ return val;
19
+ }, schema);
20
+ }
3
21
  export function registerCommunityTools(server) {
4
22
  server.addTool({
5
23
  name: "search_communities",
@@ -98,10 +116,10 @@ export function registerCommunityTools(server) {
98
116
  "Idempotent — already-linked articles are reported but don't cause errors. Requires login.",
99
117
  parameters: z.object({
100
118
  article_id: z.string().describe("Article slug/ID to link (e.g. 'machine-learning')"),
101
- community_slugs: z
119
+ community_slugs: coerceJson(z
102
120
  .array(z.string())
103
121
  .min(1)
104
- .max(50)
122
+ .max(50))
105
123
  .describe("List of community slugs to link the article to (max 50)"),
106
124
  }),
107
125
  async execute({ article_id, community_slugs }) {
@@ -1,6 +1,24 @@
1
1
  import { z } from "zod";
2
2
  import { imageContent } from "fastmcp";
3
3
  import { request } from "../auth.js";
4
+ /**
5
+ * Workaround for Claude Agent SDK MCP transport bug (#18260):
6
+ * Array/object parameters are sometimes serialized as JSON strings
7
+ * instead of native values. This preprocessor coerces them back.
8
+ */
9
+ function coerceJson(schema) {
10
+ return z.preprocess((val) => {
11
+ if (typeof val === "string") {
12
+ try {
13
+ return JSON.parse(val);
14
+ }
15
+ catch {
16
+ return val;
17
+ }
18
+ }
19
+ return val;
20
+ }, schema);
21
+ }
4
22
  export function registerResearchTools(server) {
5
23
  server.addTool({
6
24
  name: "search_web",
@@ -70,7 +88,7 @@ export function registerResearchTools(server) {
70
88
  "- For the infobox hero image, set `infobox.header.image_url` in frontmatter instead\n\n" +
71
89
  "Requires login. Rate limit: 10/min.",
72
90
  parameters: z.object({
73
- queries: z.array(z.string()).min(1).max(10).describe("Image search queries (1-10)"),
91
+ queries: coerceJson(z.array(z.string()).min(1).max(10)).describe("Image search queries (1-10)"),
74
92
  source: z.enum(["wikimedia", "google"]).default("wikimedia").describe("Image source: 'wikimedia' (free, open-licensed — preferred) or 'google' (broader coverage)"),
75
93
  limit: z.number().default(5).describe("Max results per query (1-10, default 5)"),
76
94
  }),
@@ -87,7 +105,7 @@ export function registerResearchTools(server) {
87
105
  description: "View images to verify they're suitable. Use after search_images to inspect candidates " +
88
106
  "before including them. Returns each image so you can see what it shows and write accurate captions.",
89
107
  parameters: z.object({
90
- urls: z.array(z.string().url()).min(1).max(10).describe("Image URLs to view (1-10)"),
108
+ urls: coerceJson(z.array(z.string().url()).min(1).max(10)).describe("Image URLs to view (1-10)"),
91
109
  }),
92
110
  async execute({ urls }) {
93
111
  const results = await Promise.allSettled(urls.map(async (url) => {
@@ -118,11 +136,11 @@ export function registerResearchTools(server) {
118
136
  "Each source becomes a clickable citation bubble when you use [@key] markers in your text. " +
119
137
  "Collect sources from your read_webpage calls and any subagent results, then register them all in one call.",
120
138
  parameters: z.object({
121
- sources: z.array(z.object({
139
+ sources: coerceJson(z.array(z.object({
122
140
  key: z.string().describe("Citation key — kebab-case, BibTeX-style: {domain}-{title-words} (e.g. 'nytimes-climate-report')"),
123
141
  url: z.string().describe("Source URL"),
124
142
  title: z.string().describe("Source title — include publication name after em dash (e.g. 'Climate Report — The New York Times')"),
125
- })).min(1).describe("Sources to register for citation"),
143
+ })).min(1)).describe("Sources to register for citation"),
126
144
  }),
127
145
  async execute({ sources }) {
128
146
  return `Registered ${sources.length} source${sources.length === 1 ? "" : "s"}. Use [@key] markers in your response to cite them.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openalmanac",
3
- "version": "0.2.32",
3
+ "version": "0.2.33",
4
4
  "description": "OpenAlmanac — pull, edit, and push articles to the open knowledge base",
5
5
  "type": "module",
6
6
  "bin": {