openalmanac 0.2.24 → 0.2.26
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 +17 -3
- package/dist/setup.js +7 -6
- package/dist/tools/articles.js +69 -72
- package/dist/tools/research.js +32 -23
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -38,6 +38,18 @@ export function createServer() {
|
|
|
38
38
|
"",
|
|
39
39
|
"Keep researching throughout. Do not answer from memory when you could search and give a better answer. The user will notice when you stop looking things up.",
|
|
40
40
|
"",
|
|
41
|
+
"## Enriching your responses",
|
|
42
|
+
"",
|
|
43
|
+
"Your answers should feel like living knowledge — with linked entities and images, not plain text walls.",
|
|
44
|
+
"",
|
|
45
|
+
"**Entity links:** Before writing your response, call `search_articles` with the key entity names you plan to mention (e.g. `queries: [\"Theravada Buddhism\", \"Thailand\", \"Angkor Wat\"]`). This returns which articles/stubs exist and their slugs. Then use `[[slug|Display Text]]` wikilink syntax in your response for entities that exist. These render as orange links the user can click to navigate. Only link entities that actually exist in the knowledge base — don't guess slugs.",
|
|
46
|
+
"",
|
|
47
|
+
"**Images:** Use `search_images` to find 1-2 relevant images for your response. Include them using the figure syntax: `` where position is `right`, `left`, or `center`. Always write a descriptive caption. Use `view_images` to verify candidates before including them.",
|
|
48
|
+
"",
|
|
49
|
+
"**Keep it efficient:** Batch your entity and image searches into single tool calls (both accept arrays). One `search_articles` call with 5-10 entity names and one `search_images` call is typical — don't make separate calls for each entity.",
|
|
50
|
+
"",
|
|
51
|
+
"**When to skip enrichment:** For short clarifying responses, follow-up questions, or casual conversation, don't search for entities or images. Enrich substantive, informational responses only.",
|
|
52
|
+
"",
|
|
41
53
|
"### Example — bad (performing enthusiasm):",
|
|
42
54
|
'> "Did you know that Bangkok\'s full name is the longest city name in the world? It\'s fascinating — here are some key findings I\'ve identified about Thailand\'s Hindu influence..."',
|
|
43
55
|
"",
|
|
@@ -53,6 +65,8 @@ export function createServer() {
|
|
|
53
65
|
"",
|
|
54
66
|
"The user is here because they want to dive down rabbit holes and learn about things. The article is the end product — a way to package and share what they learned — not the starting point. The conversation IS the experience. The article comes when there's enough depth and the user wants to share it.",
|
|
55
67
|
"",
|
|
68
|
+
"**Always start by talking, then research, then talk again.** Don't silently start searching. Acknowledge what the user said, tell them you're going to dig in: \"That's a really interesting area — let me do some research and then let's explore this together.\" Then research, then come back and TALK about what you found. Share the interesting parts, the surprising details, the different angles. The user should feel like you're exploring together, not like you disappeared into a factory. This applies even if the user explicitly says \"write an article\" — the exploration comes first.",
|
|
69
|
+
"",
|
|
56
70
|
"**Do not suggest writing an article on the first turn, or even the first few turns.** Your job at the start is to explore the topic — research it, share what you find, follow the user's questions. The exploration itself is what makes them want to keep going. Only after you've gone deep enough and specific subjects have come into focus should you propose an article. If you suggest it too early, it feels like being funneled into a workflow. Don't mention articles every turn either — suggest once, and if the user doesn't bite, keep exploring.",
|
|
57
71
|
"",
|
|
58
72
|
'**User has a broad interest** ("UX design", "religion in Thailand") → Don\'t ask "what angle do you want?" — research it and come back with real information about different directions. Give enough specific detail about each direction that the user can feel which one pulls them. Then follow their curiosity deeper.',
|
|
@@ -61,7 +75,7 @@ export function createServer() {
|
|
|
61
75
|
"",
|
|
62
76
|
"**User has no topic** → Talk to them. What are they into — a movie they just watched, a hobby, something from work, a place they visited, a news story that caught their eye? Once you have a thread, research it and come back with real information. You can also use requested_articles to find stubs with high demand (many articles link to them), research a few, and share what you find.",
|
|
63
77
|
"",
|
|
64
|
-
"**User wants to edit an existing article** →
|
|
78
|
+
"**User wants to edit an existing article** → Download it and read it. Look for what's *interesting but underdeveloped* — a one-sentence mention of a controversy probably has a whole story behind it. Share what you find and propose going deeper.",
|
|
65
79
|
"",
|
|
66
80
|
'**Articles emerge from research naturally.** As you research and talk, specific subjects will come into focus — a person with a fascinating story, a place with layers of history, a concept that deserves its own explanation. When you notice one of these has enough depth, say so: "the Erawan Shrine could be its own article" or "Wirathu is worth writing up." A single research conversation might produce one article or several. The conversation itself can go anywhere — opinions, tangents, speculation are all fine while talking. The articles that come out of it are encyclopedic: neutral, factual, sourced.',
|
|
67
81
|
"",
|
|
@@ -97,7 +111,7 @@ export function createServer() {
|
|
|
97
111
|
"",
|
|
98
112
|
"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.",
|
|
99
113
|
"",
|
|
100
|
-
"7. **
|
|
114
|
+
"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.",
|
|
101
115
|
"",
|
|
102
116
|
"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.",
|
|
103
117
|
"",
|
|
@@ -105,7 +119,7 @@ export function createServer() {
|
|
|
105
119
|
"",
|
|
106
120
|
"Reading and searching articles is open. Writing requires an API key (from login). Login registers an agent linked to your human user, so contributions are attributed to both.",
|
|
107
121
|
"",
|
|
108
|
-
"Core flow: login (once) → search_articles (check if exists) → search_web + read_webpage (research) → new (scaffold) or
|
|
122
|
+
"Core flow: login (once) → search_articles (check if exists) → search_web + read_webpage (research) → new (scaffold) or download (existing) → edit ~/.openalmanac/articles/{slug}.md → publish (validate & publish).",
|
|
109
123
|
"",
|
|
110
124
|
"After publishing, share the celebration URL. Then call search_communities, suggest relevant ones, and link_article if the user confirms.",
|
|
111
125
|
"",
|
package/dist/setup.js
CHANGED
|
@@ -6,10 +6,11 @@ import { getAuthStatus } from "./auth.js";
|
|
|
6
6
|
const TOOL_GROUPS = [
|
|
7
7
|
{
|
|
8
8
|
name: "Search & Read",
|
|
9
|
-
description: "search articles,
|
|
9
|
+
description: "search articles, download, view status",
|
|
10
10
|
tools: [
|
|
11
11
|
"mcp__almanac__search_articles",
|
|
12
|
-
"
|
|
12
|
+
"mcp__almanac__download",
|
|
13
|
+
"mcp__almanac__read",
|
|
13
14
|
"mcp__almanac__status",
|
|
14
15
|
"mcp__almanac__requested_articles",
|
|
15
16
|
],
|
|
@@ -21,16 +22,16 @@ const TOOL_GROUPS = [
|
|
|
21
22
|
"mcp__almanac__search_web",
|
|
22
23
|
"mcp__almanac__read_webpage",
|
|
23
24
|
"mcp__almanac__search_images",
|
|
24
|
-
"
|
|
25
|
+
"mcp__almanac__view_images",
|
|
25
26
|
],
|
|
26
27
|
},
|
|
27
28
|
{
|
|
28
29
|
name: "Write & Publish",
|
|
29
|
-
description: "create articles,
|
|
30
|
+
description: "create articles, publish edits, stubs",
|
|
30
31
|
tools: [
|
|
31
32
|
"mcp__almanac__new",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
33
|
+
"mcp__almanac__publish",
|
|
34
|
+
"mcp__almanac__create_stubs",
|
|
34
35
|
],
|
|
35
36
|
},
|
|
36
37
|
{
|
package/dist/tools/articles.js
CHANGED
|
@@ -106,7 +106,7 @@ The early life of Alan Turing began...
|
|
|
106
106
|
**Placement:** 1-3 images per major section, spread throughout. First image near the top.
|
|
107
107
|
For the infobox hero image, use \`infobox.header.image_url\` in frontmatter instead.
|
|
108
108
|
|
|
109
|
-
External image URLs are auto-persisted on
|
|
109
|
+
External image URLs are auto-persisted on publish — no extra steps needed.
|
|
110
110
|
|
|
111
111
|
## Writing quality
|
|
112
112
|
|
|
@@ -124,84 +124,81 @@ function ensureArticlesDir() {
|
|
|
124
124
|
export function registerArticleTools(server) {
|
|
125
125
|
server.addTool({
|
|
126
126
|
name: "search_articles",
|
|
127
|
-
description: "Search existing OpenAlmanac articles and stubs.
|
|
128
|
-
"
|
|
127
|
+
description: "Search existing OpenAlmanac articles and stubs. Accepts multiple queries for batch lookup. " +
|
|
128
|
+
"Use this to check if articles or stubs exist before creating them, or to find entity slugs for wikilinks. " +
|
|
129
|
+
"Results include 'stub' field (true/false) and 'entity_type' field. No authentication needed.",
|
|
129
130
|
parameters: z.object({
|
|
130
|
-
|
|
131
|
-
limit: z.number().default(
|
|
132
|
-
page: z.number().default(1).describe("Page number, 1-indexed (default 1)"),
|
|
131
|
+
queries: z.array(z.string()).min(1).max(20).describe("Search queries (1-20)"),
|
|
132
|
+
limit: z.number().default(5).describe("Max results per query (1-50, default 5)"),
|
|
133
133
|
include_stubs: z.boolean().default(true).describe("Include stub articles in results (default true)"),
|
|
134
134
|
}),
|
|
135
|
-
async execute({
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const resp = await request("GET", "/api/search", { params });
|
|
135
|
+
async execute({ queries, limit, include_stubs }) {
|
|
136
|
+
const resp = await request("POST", "/api/search/batch", {
|
|
137
|
+
json: { queries, limit, include_stubs },
|
|
138
|
+
});
|
|
140
139
|
return JSON.stringify(await resp.json(), null, 2);
|
|
141
140
|
},
|
|
142
141
|
});
|
|
143
142
|
server.addTool({
|
|
144
|
-
name: "
|
|
145
|
-
description: "
|
|
146
|
-
"Use this
|
|
147
|
-
"
|
|
148
|
-
"Idempotent: if the slug already exists, returns the existing article/stub. Requires login.",
|
|
143
|
+
name: "read",
|
|
144
|
+
description: "Read article content from OpenAlmanac. Returns the content, sources, and metadata for each slug. " +
|
|
145
|
+
"Use this to reference or summarize existing articles in conversation. " +
|
|
146
|
+
"For editing articles locally, use 'download' instead. No authentication needed.",
|
|
149
147
|
parameters: z.object({
|
|
150
|
-
|
|
151
|
-
.string()
|
|
152
|
-
.min(1)
|
|
153
|
-
.max(500)
|
|
154
|
-
.describe("Unique kebab-case identifier. For people with LinkedIn: use their vanity ID (e.g. 'john-smith-4a8b2c1'). " +
|
|
155
|
-
"For others: descriptive kebab-case (e.g. 'reinforcement-learning', 'openai')"),
|
|
156
|
-
title: z.string().describe("Display title (e.g. 'John Smith', 'Reinforcement Learning')"),
|
|
157
|
-
entity_type: z
|
|
158
|
-
.enum(["person", "organization", "topic", "event", "creative_work", "place"])
|
|
159
|
-
.optional()
|
|
160
|
-
.describe("Entity type: 'person' for individuals, 'organization' for companies/institutions/nonprofits, " +
|
|
161
|
-
"'topic' for concepts/fields/technologies, 'event' for conferences/historical events, " +
|
|
162
|
-
"'creative_work' for books/papers/films/software, 'place' for cities/countries/landmarks"),
|
|
163
|
-
headline: z
|
|
164
|
-
.string()
|
|
165
|
-
.optional()
|
|
166
|
-
.describe("Short headline (e.g. 'Professor of CS at MIT', 'AI research laboratory')"),
|
|
167
|
-
image_url: z.string().optional().describe("Image URL for the entity"),
|
|
168
|
-
summary: z
|
|
169
|
-
.string()
|
|
170
|
-
.optional()
|
|
171
|
-
.describe("2-4 sentence summary of the entity. This becomes the stub page content. " +
|
|
172
|
-
"Be informative — include key facts, dates, and context."),
|
|
148
|
+
slugs: z.array(z.string()).min(1).max(20).describe("Article slugs to read (1-20)"),
|
|
173
149
|
}),
|
|
174
|
-
async execute({
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
150
|
+
async execute({ slugs }) {
|
|
151
|
+
const resp = await request("POST", "/api/articles/batch", {
|
|
152
|
+
json: { slugs },
|
|
153
|
+
});
|
|
154
|
+
return JSON.stringify(await resp.json(), null, 2);
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
server.addTool({
|
|
158
|
+
name: "create_stubs",
|
|
159
|
+
description: "Create stub articles — placeholders for entities that don't have full articles yet. " +
|
|
160
|
+
"Use this for every entity (person, organization, topic, etc.) mentioned in an article. " +
|
|
161
|
+
"Idempotent: existing slugs return their current status. Requires login.",
|
|
162
|
+
parameters: z.object({
|
|
163
|
+
stubs: z.array(z.object({
|
|
164
|
+
slug: z
|
|
165
|
+
.string()
|
|
166
|
+
.min(1)
|
|
167
|
+
.max(500)
|
|
168
|
+
.describe("Unique kebab-case identifier. For people with LinkedIn: use their vanity ID (e.g. 'john-smith-4a8b2c1'). " +
|
|
169
|
+
"For others: descriptive kebab-case (e.g. 'reinforcement-learning', 'openai')"),
|
|
170
|
+
title: z.string().describe("Display title (e.g. 'John Smith', 'Reinforcement Learning')"),
|
|
171
|
+
entity_type: z
|
|
172
|
+
.enum(["person", "organization", "topic", "event", "creative_work", "place"])
|
|
173
|
+
.optional()
|
|
174
|
+
.describe("Entity type: 'person' for individuals, 'organization' for companies/institutions/nonprofits, " +
|
|
175
|
+
"'topic' for concepts/fields/technologies, 'event' for conferences/historical events, " +
|
|
176
|
+
"'creative_work' for books/papers/films/software, 'place' for cities/countries/landmarks"),
|
|
177
|
+
headline: z
|
|
178
|
+
.string()
|
|
179
|
+
.optional()
|
|
180
|
+
.describe("Short headline (e.g. 'Professor of CS at MIT', 'AI research laboratory')"),
|
|
181
|
+
image_url: z.string().url().optional().describe("Image URL for the entity"),
|
|
182
|
+
summary: z
|
|
183
|
+
.string()
|
|
184
|
+
.optional()
|
|
185
|
+
.describe("2-4 sentence summary of the entity. This becomes the stub page content. " +
|
|
186
|
+
"Be informative — include key facts, dates, and context."),
|
|
187
|
+
})).min(1).max(50).describe("Stubs to create (1-50)"),
|
|
188
|
+
}),
|
|
189
|
+
async execute({ stubs }) {
|
|
190
|
+
const resp = await request("POST", "/api/articles/stubs", {
|
|
188
191
|
auth: true,
|
|
189
|
-
json,
|
|
192
|
+
json: { stubs },
|
|
190
193
|
});
|
|
191
|
-
|
|
192
|
-
const statusMsg = data.status === "created"
|
|
193
|
-
? "Stub created."
|
|
194
|
-
: data.status === "stub_exists"
|
|
195
|
-
? "Stub already exists."
|
|
196
|
-
: "Full article already exists.";
|
|
197
|
-
return `${statusMsg} Slug: ${slug}\nUse [[${slug}|${title}]] to link to this entity.\n\n${JSON.stringify(data, null, 2)}`;
|
|
194
|
+
return JSON.stringify(await resp.json(), null, 2);
|
|
198
195
|
},
|
|
199
196
|
});
|
|
200
197
|
server.addTool({
|
|
201
|
-
name: "
|
|
202
|
-
description: "Download an article
|
|
203
|
-
"
|
|
204
|
-
"
|
|
198
|
+
name: "download",
|
|
199
|
+
description: "Download an article to your local workspace for editing. The file is saved to ~/.openalmanac/articles/{slug}.md " +
|
|
200
|
+
"with YAML frontmatter. Returns a writing guide covering article structure, infobox format, citations, and quality rules. " +
|
|
201
|
+
"After editing, use 'publish' to push your changes live.",
|
|
205
202
|
parameters: z.object({
|
|
206
203
|
slug: z.string().describe("Article slug (e.g. 'machine-learning')"),
|
|
207
204
|
}),
|
|
@@ -228,7 +225,7 @@ export function registerArticleTools(server) {
|
|
|
228
225
|
name: "new",
|
|
229
226
|
description: "Create a new article scaffold in your local working directory (~/.openalmanac/articles/). " +
|
|
230
227
|
"The file is created with YAML frontmatter and an empty body. Returns a writing guide covering article structure, infobox format, citations, and quality rules. " +
|
|
231
|
-
"Edit the file to add content and sources, then use
|
|
228
|
+
"Edit the file to add content and sources, then use publish to go live.",
|
|
232
229
|
parameters: z.object({
|
|
233
230
|
slug: z
|
|
234
231
|
.string()
|
|
@@ -242,7 +239,7 @@ export function registerArticleTools(server) {
|
|
|
242
239
|
ensureArticlesDir();
|
|
243
240
|
const filePath = join(ARTICLES_DIR, `${slug}.md`);
|
|
244
241
|
if (existsSync(filePath)) {
|
|
245
|
-
throw new Error(`File already exists: ${filePath}\nUse
|
|
242
|
+
throw new Error(`File already exists: ${filePath}\nUse download to refresh it, or publish to push changes.`);
|
|
246
243
|
}
|
|
247
244
|
const frontmatter = yamlStringify({ article_id: slug, title, sources: [] });
|
|
248
245
|
const scaffold = `---\n${frontmatter}---\n\n`;
|
|
@@ -251,9 +248,9 @@ export function registerArticleTools(server) {
|
|
|
251
248
|
},
|
|
252
249
|
});
|
|
253
250
|
server.addTool({
|
|
254
|
-
name: "
|
|
255
|
-
description: "Validate and publish
|
|
256
|
-
"validates
|
|
251
|
+
name: "publish",
|
|
252
|
+
description: "Validate and publish an article from your local workspace. Reads ~/.openalmanac/articles/{slug}.md, " +
|
|
253
|
+
"validates content and sources, and publishes to OpenAlmanac. Requires login.",
|
|
257
254
|
parameters: z.object({
|
|
258
255
|
slug: z.string().describe("Article slug matching the filename (without .md)"),
|
|
259
256
|
change_title: z.string().optional().describe("Short title for the change (e.g. 'Added early life section')"),
|
|
@@ -266,7 +263,7 @@ export function registerArticleTools(server) {
|
|
|
266
263
|
raw = readFileSync(filePath, "utf-8");
|
|
267
264
|
}
|
|
268
265
|
catch {
|
|
269
|
-
throw new Error(`File not found: ${filePath}\nUse
|
|
266
|
+
throw new Error(`File not found: ${filePath}\nUse download to get an existing article or new to create a scaffold.`);
|
|
270
267
|
}
|
|
271
268
|
// Local validation
|
|
272
269
|
const errors = validateArticle(raw);
|
|
@@ -323,7 +320,7 @@ export function registerArticleTools(server) {
|
|
|
323
320
|
: "Not logged in. Use login to authenticate.";
|
|
324
321
|
const files = readdirSync(ARTICLES_DIR).filter((f) => f.endsWith(".md"));
|
|
325
322
|
if (files.length === 0) {
|
|
326
|
-
return `${authLine}\n\nLocal articles (0 files in ${ARTICLES_DIR}):\n (none — use
|
|
323
|
+
return `${authLine}\n\nLocal articles (0 files in ${ARTICLES_DIR}):\n (none — use download or new to get started)`;
|
|
327
324
|
}
|
|
328
325
|
const rows = [];
|
|
329
326
|
for (const file of files) {
|
package/dist/tools/research.js
CHANGED
|
@@ -43,10 +43,10 @@ export function registerResearchTools(server) {
|
|
|
43
43
|
});
|
|
44
44
|
server.addTool({
|
|
45
45
|
name: "search_images",
|
|
46
|
-
description: "Search for images to include in articles. Returns image URLs, titles, dimensions, and licensing info. " +
|
|
46
|
+
description: "Search for images to include in articles. Accepts multiple queries for batch lookup. Returns image URLs, titles, dimensions, and licensing info. " +
|
|
47
47
|
"Two sources: 'wikimedia' (free, open-licensed images from Wikimedia Commons — preferred) and 'google' (broader web images via Google). " +
|
|
48
|
-
"Use descriptive search terms. After searching, call
|
|
49
|
-
"External image URLs are automatically persisted when you
|
|
48
|
+
"Use descriptive search terms. After searching, call view_images on promising candidates to see what they actually show before using them. " +
|
|
49
|
+
"External image URLs are automatically persisted when you publish the article — no extra steps needed.\n\n" +
|
|
50
50
|
"## Using images in articles\n\n" +
|
|
51
51
|
"Images render as figures with visible captions. The alt text becomes the caption — make it descriptive.\n\n" +
|
|
52
52
|
"**Syntax:** ``\n\n" +
|
|
@@ -70,37 +70,46 @@ export function registerResearchTools(server) {
|
|
|
70
70
|
"- For the infobox hero image, set `infobox.header.image_url` in frontmatter instead\n\n" +
|
|
71
71
|
"Requires login. Rate limit: 10/min.",
|
|
72
72
|
parameters: z.object({
|
|
73
|
-
|
|
73
|
+
queries: z.array(z.string()).min(1).max(10).describe("Image search queries (1-10)"),
|
|
74
74
|
source: z.enum(["wikimedia", "google"]).default("wikimedia").describe("Image source: 'wikimedia' (free, open-licensed — preferred) or 'google' (broader coverage)"),
|
|
75
|
-
limit: z.number().default(
|
|
75
|
+
limit: z.number().default(5).describe("Max results per query (1-10, default 5)"),
|
|
76
76
|
}),
|
|
77
|
-
async execute({
|
|
78
|
-
const resp = await request("
|
|
77
|
+
async execute({ queries, source, limit }) {
|
|
78
|
+
const resp = await request("POST", "/api/research/images/batch", {
|
|
79
79
|
auth: true,
|
|
80
|
-
|
|
80
|
+
json: { queries, source, limit },
|
|
81
81
|
});
|
|
82
82
|
return JSON.stringify(await resp.json(), null, 2);
|
|
83
83
|
},
|
|
84
84
|
});
|
|
85
85
|
server.addTool({
|
|
86
|
-
name: "
|
|
87
|
-
description: "View
|
|
88
|
-
"before including them. Returns
|
|
86
|
+
name: "view_images",
|
|
87
|
+
description: "View images to verify they're suitable. Use after search_images to inspect candidates " +
|
|
88
|
+
"before including them. Returns each image so you can see what it shows and write accurate captions.",
|
|
89
89
|
parameters: z.object({
|
|
90
|
-
|
|
90
|
+
urls: z.array(z.string().url()).min(1).max(10).describe("Image URLs to view (1-10)"),
|
|
91
91
|
}),
|
|
92
|
-
async execute({
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
async execute({ urls }) {
|
|
93
|
+
const results = await Promise.allSettled(urls.map(async (url) => {
|
|
94
|
+
try {
|
|
95
|
+
return { url, image: await imageContent({ url }) };
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { url, error: `Failed to fetch image from ${url}` };
|
|
99
|
+
}
|
|
100
|
+
}));
|
|
101
|
+
const content = [];
|
|
102
|
+
for (const result of results) {
|
|
103
|
+
const val = result.status === "fulfilled" ? result.value : { url: "unknown", error: "Failed" };
|
|
104
|
+
if ("image" in val) {
|
|
105
|
+
content.push(val.image);
|
|
106
|
+
content.push({ type: "text", text: `Image URL: ${val.url}` });
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
content.push({ type: "text", text: val.error });
|
|
110
|
+
}
|
|
103
111
|
}
|
|
112
|
+
return { content };
|
|
104
113
|
},
|
|
105
114
|
});
|
|
106
115
|
}
|