openalmanac 0.2.34 → 0.2.36

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.d.ts CHANGED
@@ -16,7 +16,7 @@ export declare function getAuthStatus(): Promise<AuthStatus>;
16
16
  export declare function buildAuthHeaders(): Record<string, string>;
17
17
  export declare function request(method: string, path: string, options?: {
18
18
  auth?: boolean;
19
- params?: Record<string, string | number>;
19
+ params?: Record<string, string | number | boolean>;
20
20
  json?: unknown;
21
21
  body?: string;
22
22
  contentType?: string;
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createServer } from "./server.js";
3
3
  import { runLogin, runLogout } from "./login.js";
4
- import { runSetup } from "./setup.js";
4
+ import { runSetup, runRedditSetup } from "./setup.js";
5
5
  const command = process.argv[2];
6
6
  if (command === "setup") {
7
7
  runSetup().catch((e) => {
@@ -9,6 +9,12 @@ if (command === "setup") {
9
9
  process.exit(1);
10
10
  });
11
11
  }
12
+ else if (command === "reddit") {
13
+ runRedditSetup().catch((e) => {
14
+ console.error(e instanceof Error ? e.message : e);
15
+ process.exit(1);
16
+ });
17
+ }
12
18
  else if (command === "login") {
13
19
  runLogin().catch((e) => {
14
20
  console.error(e instanceof Error ? e.message : e);
package/dist/server.js CHANGED
@@ -47,7 +47,7 @@ export function createServer() {
47
47
  "",
48
48
  "Your answers should feel like living knowledge — with linked entities and images, not plain text walls.",
49
49
  "",
50
- "**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.",
50
+ "**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. Published articles preserve wikilinks in storage; dead links auto-create stub articles on publish (community wikis) so round-trip editing stays faithful.",
51
51
  "",
52
52
  "**Images:** Use `search_images` to find 1-2 relevant images for your response. Include them using the figure syntax: `![Descriptive caption](image_url \"position\")` where position is `right`, `left`, or `center`. Always write a descriptive caption. Use `view_images` to verify candidates before including them.",
53
53
  "",
@@ -78,7 +78,7 @@ export function createServer() {
78
78
  "",
79
79
  'Example: User says "I\'m interested in UX." Don\'t say "Would you like to focus on history, applications, or companies?" Instead, research and say: "So UX was coined by Don Norman at Apple in 1993, but the practice goes back to Henry Dreyfuss in the 1950s designing telephone handsets by measuring thousands of human bodies. There\'s also the dark patterns side — Ryanair\'s checkout flow got studied in academic papers as a case study in hostile design. And there\'s the curb cut effect — features designed for disabled users that end up benefiting everyone. What pulls you?"',
80
80
  "",
81
- "**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.",
81
+ "**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 `list_articles` with `sort: \"most_referenced\"` and `stubs_only: true` on a community wiki to find high-demand stubs.",
82
82
  "",
83
83
  "**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.",
84
84
  "",
@@ -103,7 +103,7 @@ export function createServer() {
103
103
  "",
104
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. **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.",
106
+ "4. **Scaffold** — Use `new` with one or more `{ title, slug?, topics? }` entries. Provide `slug` when you know the canonical ID; otherwise it is auto-derived from the title. For community wikis pass `community_slug`. 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
  "",
@@ -114,11 +114,11 @@ export function createServer() {
114
114
  " - **Review agent** → tell it to read https://www.openalmanac.org/review-guidelines.md and review the draft at `~/.openalmanac/articles/{slug}.md`",
115
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`",
116
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`",
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`",
117
+ " - **Linking agent** → tell it to read https://www.openalmanac.org/linking-guidelines.md and add wikilinks for the draft at `~/.openalmanac/articles/{slug}.md` (dead links become stubs on publish — no manual `create_stubs` step)",
118
118
  "",
119
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.",
120
120
  "",
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.",
121
+ "8. **Publish** — Validate and publish (`publish` with `slugs` or `community_slug` to batch). Put per-article change notes in frontmatter as `edit_summary` (maps to the API). Share the exact URL from the publish response when single-article. For community wikis, use `list_articles` to verify coverage.",
122
122
  "",
123
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.",
124
124
  "",
@@ -126,9 +126,9 @@ export function createServer() {
126
126
  "",
127
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
- "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).",
129
+ "Core flow: login (once) → search_articles (check if exists) → search_web + read_webpage (research) → `new` (batch scaffold, auto slugs) or `download` (batch) → edit files under ~/.openalmanac/articles/ → `publish` (slugs or community_slug for folder batch).",
130
130
  "",
131
- "After publishing, share the celebration URL. Then call search_communities, suggest relevant ones, and link_article if the user confirms.",
131
+ "After publishing, share the celebration URL when applicable. Community-owned articles are created under a community path use `list_articles` to browse a community wiki.",
132
132
  "",
133
133
  "When working with tool results, write down any important information you might need later, as the original tool result may be cleared.",
134
134
  ].join("\n"),
package/dist/setup.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export declare function runSetup(): Promise<void>;
2
+ export declare function runRedditSetup(): Promise<void>;
package/dist/setup.js CHANGED
@@ -1,37 +1,37 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, cpSync } from "fs";
2
2
  import { homedir } from "os";
3
- import { join } from "path";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
4
5
  import { performLogin } from "./login-core.js";
5
6
  import { getAuthStatus } from "./auth.js";
6
7
  const TOOL_GROUPS = [
7
8
  {
8
9
  name: "Search & Read",
9
- description: "search articles, download, view status",
10
+ description: "search, read, download, and browse community wiki articles",
10
11
  tools: [
11
12
  "mcp__almanac__search_articles",
12
13
  "mcp__almanac__download",
13
14
  "mcp__almanac__read",
14
- "mcp__almanac__status",
15
- "mcp__almanac__requested_articles",
15
+ "mcp__almanac__list_articles",
16
16
  ],
17
17
  },
18
18
  {
19
19
  name: "Research",
20
- description: "web search, read pages, find images",
20
+ description: "web search, read pages, register sources, find images",
21
21
  tools: [
22
22
  "mcp__almanac__search_web",
23
23
  "mcp__almanac__read_webpage",
24
+ "mcp__almanac__register_sources",
24
25
  "mcp__almanac__search_images",
25
26
  "mcp__almanac__view_images",
26
27
  ],
27
28
  },
28
29
  {
29
30
  name: "Write & Publish",
30
- description: "create articles, publish edits, stubs",
31
+ description: "create article drafts and publish edits",
31
32
  tools: [
32
33
  "mcp__almanac__new",
33
34
  "mcp__almanac__publish",
34
- "mcp__almanac__create_stubs",
35
35
  ],
36
36
  },
37
37
  {
@@ -41,12 +41,11 @@ const TOOL_GROUPS = [
41
41
  },
42
42
  {
43
43
  name: "Community",
44
- description: "communities, posts, article linking",
44
+ description: "communities and posts",
45
45
  tools: [
46
46
  "mcp__almanac__search_communities",
47
47
  "mcp__almanac__create_community",
48
48
  "mcp__almanac__create_post",
49
- "mcp__almanac__link_article",
50
49
  ],
51
50
  },
52
51
  {
@@ -562,3 +561,109 @@ export async function runSetup() {
562
561
  printResult(agent, loginResult, mcpChanged, count);
563
562
  process.exit(0);
564
563
  }
564
+ /* ── Skill installation ────────────────────────────────────────── */
565
+ function getPackageSkillsDir() {
566
+ const thisFile = fileURLToPath(import.meta.url);
567
+ // dist/setup.js → package root → skills/
568
+ return join(dirname(thisFile), "..", "skills");
569
+ }
570
+ function installSkill(skillName) {
571
+ const src = join(getPackageSkillsDir(), skillName);
572
+ if (!existsSync(src)) {
573
+ throw new Error(`Skill "${skillName}" not found in package at ${src}`);
574
+ }
575
+ const dest = join(homedir(), ".claude", "skills", skillName);
576
+ // Always overwrite to ensure latest version
577
+ mkdirSync(dirname(dest), { recursive: true });
578
+ cpSync(src, dest, { recursive: true, force: true });
579
+ return true;
580
+ }
581
+ /* ── Reddit-specific tool groups ───────────────────────────────── */
582
+ const REDDIT_EXTRA_TOOLS = [
583
+ "Bash(node *)",
584
+ "Bash(curl *)",
585
+ ];
586
+ /* ── Reddit setup banner ───────────────────────────────────────── */
587
+ function printRedditBanner() {
588
+ process.stdout.write("\n");
589
+ for (let i = 0; i < LOGO_LINES.length; i++) {
590
+ process.stdout.write(`${GRADIENT[i]}${LOGO_LINES[i]}${RST}\n`);
591
+ }
592
+ process.stdout.write(`\n${DIM} Turn any subreddit into a wiki${RST}\n`);
593
+ }
594
+ /* ── Reddit result screen ──────────────────────────────────────── */
595
+ function printRedditResult(agent, loginResult, mcpChanged, toolCount) {
596
+ process.stdout.write("\x1b[2J\x1b[H");
597
+ printRedditBanner();
598
+ printBadge();
599
+ w("");
600
+ stepDone(`Agent \u2192 ${WHITE_BOLD}${agent}${RST}`);
601
+ w(BAR);
602
+ stepDone(`MCP server ${mcpChanged ? "configured" : `${DIM}already configured${RST}`}`);
603
+ w(BAR);
604
+ stepDone(`${BLUE}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed`);
605
+ w(BAR);
606
+ stepDone(loginLabel(loginResult));
607
+ w(BAR);
608
+ stepDone(`${BLUE}/reddit-wiki${RST} skill installed`);
609
+ w(BAR);
610
+ stepDone(`${BLUE}Setup complete${RST}`);
611
+ w("");
612
+ // Next steps box
613
+ const innerW = 62;
614
+ const row = (content) => {
615
+ const padding = Math.max(0, innerW - vis(content));
616
+ return ` ${BLUE_DIM}\u2502${RST}${content}${" ".repeat(padding)}${BLUE_DIM}\u2502${RST}`;
617
+ };
618
+ const empty = row("");
619
+ w(` ${BLUE_DIM}\u256d${"─".repeat(innerW)}\u256e${RST}`);
620
+ w(empty);
621
+ w(row(` ${WHITE_BOLD}Next steps${RST}`));
622
+ w(empty);
623
+ w(row(` ${BLUE}1.${RST} Type ${WHITE_BOLD}claude${RST} to start Claude Code`));
624
+ w(row(` ${BLUE}2.${RST} Run ${BLUE}/reddit-wiki r/<subreddit>${RST}`));
625
+ w(empty);
626
+ w(` ${BLUE_DIM}\u2570${"─".repeat(innerW)}\u256f${RST}`);
627
+ w("");
628
+ }
629
+ /* ── Reddit entry point ────────────────────────────────────────── */
630
+ export async function runRedditSetup() {
631
+ const skipTui = process.argv.includes("--yes") || process.argv.includes("-y");
632
+ const interactive = process.stdin.isTTY && !skipTui;
633
+ let agent = "Claude Code";
634
+ if (interactive) {
635
+ agent = await runAgentSelect();
636
+ }
637
+ const mcpChanged = configureMcp();
638
+ let tools;
639
+ if (interactive) {
640
+ tools = await runToolSelect(agent, mcpChanged);
641
+ }
642
+ else {
643
+ tools = TOOL_GROUPS.flatMap((g) => g.tools);
644
+ }
645
+ // Add reddit-specific tool permissions
646
+ tools = [...tools, ...REDDIT_EXTRA_TOOLS];
647
+ const count = configurePermissions(tools);
648
+ // Login step
649
+ let loginResult;
650
+ if (interactive) {
651
+ loginResult = await runLoginStep(agent, mcpChanged, count);
652
+ }
653
+ else {
654
+ try {
655
+ const result = await performLogin();
656
+ loginResult =
657
+ result.status === "already_logged_in"
658
+ ? { status: "already", name: result.name }
659
+ : { status: "done" };
660
+ }
661
+ catch {
662
+ loginResult = { status: "skipped" };
663
+ }
664
+ }
665
+ // Install the reddit-wiki skill
666
+ installSkill("reddit-wiki");
667
+ printRedditResult(agent, loginResult, mcpChanged, count);
668
+ process.exit(0);
669
+ }