openalmanac 0.2.30 → 0.2.32

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.js CHANGED
@@ -42,13 +42,13 @@ export async function getAuthStatus() {
42
42
  if (!key)
43
43
  return { loggedIn: false };
44
44
  try {
45
- const resp = await fetch(`${API_BASE}/api/agents/me`, {
45
+ const resp = await fetch(`${API_BASE}/api/users/me`, {
46
46
  headers: { Authorization: `Bearer ${key}` },
47
47
  signal: AbortSignal.timeout(10_000),
48
48
  });
49
49
  if (resp.ok) {
50
50
  const data = (await resp.json());
51
- return { loggedIn: true, name: data.name ?? "unknown" };
51
+ return { loggedIn: true, name: data.display_name ?? data.username ?? "unknown" };
52
52
  }
53
53
  }
54
54
  catch {
@@ -7,7 +7,7 @@ function callbackPage(success) {
7
7
  // These values are hardcoded — never interpolate user input here.
8
8
  const title = success ? "You\u2019re connected" : "Something went wrong";
9
9
  const messageLine1 = success
10
- ? "Your agent is registered and ready to go."
10
+ ? "Your account is connected and a personal API key is ready to go."
11
11
  : "Invalid token. Please return to your terminal and try again.";
12
12
  const messageLine2 = success
13
13
  ? "You can close this tab and return to your terminal."
@@ -160,13 +160,13 @@ export async function performLogin(options) {
160
160
  const existingKey = getApiKey();
161
161
  if (existingKey) {
162
162
  try {
163
- const resp = await fetch(`${API_BASE}/api/agents/me`, {
163
+ const resp = await fetch(`${API_BASE}/api/users/me`, {
164
164
  headers: { Authorization: `Bearer ${existingKey}` },
165
165
  signal: AbortSignal.timeout(10_000),
166
166
  });
167
167
  if (resp.ok) {
168
168
  const data = (await resp.json());
169
- return { status: "already_logged_in", name: data.name ?? "unknown" };
169
+ return { status: "already_logged_in", name: data.display_name ?? data.username ?? "unknown" };
170
170
  }
171
171
  }
172
172
  catch {
package/dist/login.js CHANGED
@@ -6,7 +6,7 @@ export async function runLogin() {
6
6
  console.log(`Already logged in as ${result.name}.`);
7
7
  }
8
8
  else {
9
- console.log("Logged in. Your agent is registered and contributions will be attributed to your account.");
9
+ console.log("Logged in. A personal API key was created for this installation and contributions will be attributed to your account.");
10
10
  }
11
11
  }
12
12
  export async function runLogout() {
package/dist/server.js CHANGED
@@ -124,7 +124,7 @@ export function createServer() {
124
124
  "",
125
125
  "## Technical workflow",
126
126
  "",
127
- "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.",
127
+ "Reading and searching articles is open. Writing requires an API key (from login). Login creates a personal API key linked to your user account, so contributions are attributed to you.",
128
128
  "",
129
129
  "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).",
130
130
  "",
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync } from "node:fs";
2
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync, unlinkSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { stringify as yamlStringify } from "yaml";
5
5
  import { request, ARTICLES_DIR, getAuthStatus } from "../auth.js";
@@ -206,6 +206,9 @@ export function registerArticleTools(server) {
206
206
  slug: z.string().describe("Article slug (e.g. 'machine-learning')"),
207
207
  }),
208
208
  async execute({ slug }) {
209
+ if (!SLUG_RE.test(slug)) {
210
+ throw new Error(`Invalid slug "${slug}". Must be kebab-case (e.g. 'machine-learning').`);
211
+ }
209
212
  const resp = await request("GET", `/api/articles/${slug}`, {
210
213
  params: { format: "md" },
211
214
  });
@@ -213,6 +216,8 @@ export function registerArticleTools(server) {
213
216
  ensureArticlesDir();
214
217
  const filePath = join(ARTICLES_DIR, `${slug}.md`);
215
218
  writeFileSync(filePath, markdown, "utf-8");
219
+ const originalPath = join(ARTICLES_DIR, `.${slug}.original.md`);
220
+ writeFileSync(originalPath, markdown, "utf-8");
216
221
  const { frontmatter, content } = parseFrontmatter(markdown);
217
222
  const title = frontmatter.title || "(untitled)";
218
223
  const wordCount = content.trim().split(/\s+/).filter(Boolean).length;
@@ -260,6 +265,9 @@ export function registerArticleTools(server) {
260
265
  change_description: z.string().optional().describe("Longer description of what changed and why"),
261
266
  }),
262
267
  async execute({ slug, change_title, change_description }) {
268
+ if (!SLUG_RE.test(slug)) {
269
+ throw new Error(`Invalid slug "${slug}". Must be kebab-case (e.g. 'machine-learning').`);
270
+ }
263
271
  const filePath = join(ARTICLES_DIR, `${slug}.md`);
264
272
  let raw;
265
273
  try {
@@ -293,7 +301,25 @@ export function registerArticleTools(server) {
293
301
  const data = (await resp.json());
294
302
  const articleUrl = `https://www.openalmanac.org/article/${slug}?celebrate=true`;
295
303
  openBrowser(articleUrl);
296
- return `Pushed successfully.\n\nArticle URL (share this exact link with the user): ${articleUrl}\n\n${JSON.stringify(data, null, 2)}`;
304
+ // Clean up local files after successful publish
305
+ let cleanupWarning = "";
306
+ try {
307
+ unlinkSync(filePath);
308
+ }
309
+ catch (e) {
310
+ if (e.code !== "ENOENT") {
311
+ cleanupWarning = `\nNote: could not remove local draft: ${e.message}`;
312
+ }
313
+ }
314
+ try {
315
+ unlinkSync(join(ARTICLES_DIR, `.${slug}.original.md`));
316
+ }
317
+ catch (e) {
318
+ if (e.code !== "ENOENT") {
319
+ cleanupWarning += `\nNote: could not remove original copy: ${e.message}`;
320
+ }
321
+ }
322
+ return `Pushed successfully.\n\nArticle URL (share this exact link with the user): ${articleUrl}${cleanupWarning}\n\n${JSON.stringify(data, null, 2)}`;
297
323
  },
298
324
  });
299
325
  server.addTool({
@@ -352,7 +378,7 @@ export function registerArticleTools(server) {
352
378
  const authLine = auth.loggedIn
353
379
  ? `Logged in as ${auth.name}.`
354
380
  : "Not logged in. Use login to authenticate.";
355
- const files = readdirSync(ARTICLES_DIR).filter((f) => f.endsWith(".md"));
381
+ const files = readdirSync(ARTICLES_DIR).filter((f) => f.endsWith(".md") && !f.startsWith("."));
356
382
  if (files.length === 0) {
357
383
  return `${authLine}\n\nLocal articles (0 files in ${ARTICLES_DIR}):\n (none — use download or new to get started)`;
358
384
  }
@@ -3,7 +3,7 @@ import { performLogin } from "../login-core.js";
3
3
  export function registerAuthTools(server) {
4
4
  server.addTool({
5
5
  name: "login",
6
- description: "Log in via browser to register an agent and get an API key. This is the required " +
6
+ description: "Log in via browser to connect your account and get a personal API key. This is the required " +
7
7
  "first step before creating or updating articles. Only needs to be called once.\n\n" +
8
8
  "If you already have a valid API key, this returns immediately without opening a browser.",
9
9
  async execute() {
@@ -11,7 +11,7 @@ export function registerAuthTools(server) {
11
11
  if (result.status === "already_logged_in") {
12
12
  return `Already logged in as ${result.name}.`;
13
13
  }
14
- return "Logged in. Your agent is registered and contributions will be attributed to your account.";
14
+ return "Logged in. A personal API key was created for this installation and contributions will be attributed to your account.";
15
15
  },
16
16
  });
17
17
  server.addTool({
@@ -112,4 +112,20 @@ export function registerResearchTools(server) {
112
112
  return { content };
113
113
  },
114
114
  });
115
+ server.addTool({
116
+ name: "register_sources",
117
+ description: "Register sources you plan to cite in your response. Call this BEFORE writing your response text. " +
118
+ "Each source becomes a clickable citation bubble when you use [@key] markers in your text. " +
119
+ "Collect sources from your read_webpage calls and any subagent results, then register them all in one call.",
120
+ parameters: z.object({
121
+ sources: z.array(z.object({
122
+ key: z.string().describe("Citation key — kebab-case, BibTeX-style: {domain}-{title-words} (e.g. 'nytimes-climate-report')"),
123
+ url: z.string().describe("Source URL"),
124
+ 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"),
126
+ }),
127
+ async execute({ sources }) {
128
+ return `Registered ${sources.length} source${sources.length === 1 ? "" : "s"}. Use [@key] markers in your response to cite them.`;
129
+ },
130
+ });
115
131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openalmanac",
3
- "version": "0.2.30",
3
+ "version": "0.2.32",
4
4
  "description": "OpenAlmanac — pull, edit, and push articles to the open knowledge base",
5
5
  "type": "module",
6
6
  "bin": {