openalmanac 0.3.5 → 0.4.0
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 +2 -2
- package/dist/auth.js +2 -2
- package/dist/cli.js +0 -0
- package/dist/login-core.js +2 -1
- package/dist/onboarding-copy.d.ts +1 -0
- package/dist/onboarding-copy.js +14 -0
- package/dist/server.js +34 -31
- package/dist/setup.js +31 -59
- package/dist/tool-registry.d.ts +11 -0
- package/dist/tool-registry.js +148 -0
- package/dist/tools/auth.js +1 -1
- package/dist/tools/pages.js +218 -105
- package/dist/tools/research.js +38 -49
- package/dist/tools/topics.js +4 -34
- package/dist/tools/wikis.js +25 -27
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +19 -0
- package/package.json +15 -6
- package/skills/reddit-wiki/SKILL.md +46 -46
- package/dist/tools/articles.d.ts +0 -2
- package/dist/tools/articles.js +0 -401
- package/dist/tools/communities.d.ts +0 -2
- package/dist/tools/communities.js +0 -127
- package/dist/tools/people.d.ts +0 -2
- package/dist/tools/people.js +0 -20
package/dist/auth.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
declare const API_BASE: string;
|
|
2
2
|
declare const API_KEY_PATH: string;
|
|
3
|
-
declare const
|
|
4
|
-
export { API_BASE, API_KEY_PATH,
|
|
3
|
+
declare const PAGES_DIR: string;
|
|
4
|
+
export { API_BASE, API_KEY_PATH, PAGES_DIR };
|
|
5
5
|
export declare function getApiKey(): string | null;
|
|
6
6
|
export declare function requireApiKey(): string;
|
|
7
7
|
export declare function saveApiKey(key: string): void;
|
package/dist/auth.js
CHANGED
|
@@ -5,8 +5,8 @@ import { chmodSync } from "node:fs";
|
|
|
5
5
|
const API_BASE = process.env.OPENALMANAC_API_BASE || "https://www.openalmanac.org/api/proxy";
|
|
6
6
|
const API_KEY_DIR = join(homedir(), ".openalmanac");
|
|
7
7
|
const API_KEY_PATH = join(API_KEY_DIR, "api_key");
|
|
8
|
-
const
|
|
9
|
-
export { API_BASE, API_KEY_PATH,
|
|
8
|
+
const PAGES_DIR = join(homedir(), ".openalmanac", "pages");
|
|
9
|
+
export { API_BASE, API_KEY_PATH, PAGES_DIR };
|
|
10
10
|
export function getApiKey() {
|
|
11
11
|
const envKey = process.env.OPENALMANAC_API_KEY;
|
|
12
12
|
if (envKey)
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/login-core.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { getApiKey, saveApiKey, API_BASE } from "./auth.js";
|
|
3
3
|
import { openBrowser } from "./browser.js";
|
|
4
|
+
import { EXAMPLE_PROMPT } from "./onboarding-copy.js";
|
|
4
5
|
const CONNECT_URL_BASE = "https://openalmanac.org/contribute/connect";
|
|
5
6
|
const LOGIN_TIMEOUT_MS = 120_000;
|
|
6
7
|
function callbackPage(success) {
|
|
@@ -118,7 +119,7 @@ function callbackPage(success) {
|
|
|
118
119
|
<script>
|
|
119
120
|
const steps = [
|
|
120
121
|
{ target: 'line1', prefix: '<span class="prompt">$ </span>', text: 'claude', delay: 600 },
|
|
121
|
-
{ target: 'line2', prefix: '<span class="prompt">> </span>', text:
|
|
122
|
+
{ target: 'line2', prefix: '<span class="prompt">> </span>', text: ${JSON.stringify(EXAMPLE_PROMPT)}, delay: 400 },
|
|
122
123
|
];
|
|
123
124
|
function type(el, prefix, text, speed, cb) {
|
|
124
125
|
el.style.display = '';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const EXAMPLE_PROMPT = "Let's explore the Demon Slayer wiki using Almanac";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Onboarding copy shared between setup's "next steps" panel and the
|
|
2
|
+
// post-login connected page rendered by `login-core.ts`.
|
|
3
|
+
//
|
|
4
|
+
// The example prompt names a real wiki on purpose: the goal is to teach the
|
|
5
|
+
// new user that wikis exist as the unit Almanac is organized around, not
|
|
6
|
+
// just that the agent can do "research". If the example wiki changes, both
|
|
7
|
+
// surfaces (`setup.ts` next steps + `login-core.ts` terminal mock) update
|
|
8
|
+
// automatically because they consume this single constant.
|
|
9
|
+
//
|
|
10
|
+
// This lives in its own file rather than `setup.ts` to keep `login-core.ts`
|
|
11
|
+
// from depending on the setup TUI module (`setup.ts` already imports
|
|
12
|
+
// `performLogin` from `login-core.ts`, so adding the reverse edge would
|
|
13
|
+
// create a cycle).
|
|
14
|
+
export const EXAMPLE_PROMPT = "Let's explore the Demon Slayer wiki using Almanac";
|
package/dist/server.js
CHANGED
|
@@ -20,8 +20,8 @@ export function createServer() {
|
|
|
20
20
|
│ │
|
|
21
21
|
│ Try asking your agent: │
|
|
22
22
|
│ │
|
|
23
|
-
│ → "Write an Almanac
|
|
24
|
-
│ → "Improve the Alan Turing
|
|
23
|
+
│ → "Write an Almanac page about CORS" │
|
|
24
|
+
│ → "Improve the Alan Turing page" │
|
|
25
25
|
│ │
|
|
26
26
|
│ Docs: openalmanac.org/contribute │
|
|
27
27
|
│ │
|
|
@@ -48,11 +48,11 @@ export function createServer() {
|
|
|
48
48
|
"",
|
|
49
49
|
"Your answers should feel like living knowledge — with linked entities and images, not plain text walls.",
|
|
50
50
|
"",
|
|
51
|
-
"**Entity links:** Before writing your response, call `
|
|
51
|
+
"**Entity links:** Before writing your response, call `search_pages` with the key entity names you plan to mention (e.g. `queries: [\"Theravada Buddhism\", \"Thailand\", \"Angkor Wat\"]`). This returns which pages/stubs exist and their slugs. Then use `[[slug|Display Text]]` wikilink syntax in your response. Dead links auto-create stub pages on publish so round-trip editing stays faithful.",
|
|
52
52
|
"",
|
|
53
53
|
"**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.",
|
|
54
54
|
"",
|
|
55
|
-
"**Keep it efficient:** Batch your entity and image searches into single tool calls (both accept arrays). One `
|
|
55
|
+
"**Keep it efficient:** Batch your entity and image searches into single tool calls (both accept arrays). One `search_pages` call with 5-10 entity names and one `search_images` call is typical — don't make separate calls for each entity.",
|
|
56
56
|
"",
|
|
57
57
|
"**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.",
|
|
58
58
|
"",
|
|
@@ -69,21 +69,21 @@ export function createServer() {
|
|
|
69
69
|
"",
|
|
70
70
|
"## Entry points",
|
|
71
71
|
"",
|
|
72
|
-
"The user is here because they want to dive down rabbit holes and learn about things. The
|
|
72
|
+
"The user is here because they want to dive down rabbit holes and learn about things. The page is the end product — a way to package and share what they learned — not the starting point. The conversation IS the experience. The page comes when there's enough depth and the user wants to share it.",
|
|
73
73
|
"",
|
|
74
|
-
"**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
|
|
74
|
+
"**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 a page\" — the exploration comes first.",
|
|
75
75
|
"",
|
|
76
|
-
"**Do not suggest writing
|
|
76
|
+
"**Do not suggest writing a page 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 a page. If you suggest it too early, it feels like being funneled into a workflow. Don't mention pages every turn either — suggest once, and if the user doesn't bite, keep exploring.",
|
|
77
77
|
"",
|
|
78
78
|
'**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.',
|
|
79
79
|
"",
|
|
80
80
|
'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?"',
|
|
81
81
|
"",
|
|
82
|
-
"**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 `
|
|
82
|
+
"**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_pages` with `wiki_slug` and `stubs_only: true` to find stubs that need writing.",
|
|
83
83
|
"",
|
|
84
|
-
"**User wants to edit an existing
|
|
84
|
+
"**User wants to edit an existing page** → 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.",
|
|
85
85
|
"",
|
|
86
|
-
'**
|
|
86
|
+
'**pages 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 page" or "Wirathu is worth writing up." A single research conversation might produce one page or several. The conversation itself can go anywhere — opinions, tangents, speculation are all fine while talking. The pages that come out of it are encyclopedic: neutral, factual, sourced.',
|
|
87
87
|
"",
|
|
88
88
|
"## Guidelines",
|
|
89
89
|
"",
|
|
@@ -96,32 +96,30 @@ export function createServer() {
|
|
|
96
96
|
"",
|
|
97
97
|
"## Writing flow",
|
|
98
98
|
"",
|
|
99
|
-
"When you've researched enough and a specific
|
|
99
|
+
"When you've researched enough and a specific page topic has come into focus:",
|
|
100
100
|
"",
|
|
101
|
-
'1. **Align briefly with the user** — Talk about what the
|
|
101
|
+
'1. **Align briefly with the user** — Talk about what the page 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?"',
|
|
102
102
|
"",
|
|
103
|
-
"2. **
|
|
103
|
+
"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.",
|
|
104
104
|
"",
|
|
105
|
-
"3. **
|
|
105
|
+
"3. **Scaffold** — Use `new` with one or more `{ title, slug?, topics? }` entries and a `wiki_slug`. Provide `slug` when you know the canonical ID; otherwise it is auto-derived from the title. 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
106
|
"",
|
|
107
|
-
"4. **
|
|
107
|
+
"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.",
|
|
108
108
|
"",
|
|
109
|
-
"
|
|
109
|
+
" Write the full page 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.",
|
|
110
110
|
"",
|
|
111
|
-
"
|
|
111
|
+
"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.",
|
|
112
112
|
"",
|
|
113
|
-
"
|
|
113
|
+
" - **Review agent** → tell it to read https://www.openalmanac.org/review-guidelines.md and review the draft at `~/.openalmanac/pages/{wiki_slug}/{slug}.md`",
|
|
114
|
+
" - **Fact-check agent** → tell it to read https://www.openalmanac.org/fact-checking-guidelines.md and fact-check the draft at `~/.openalmanac/pages/{wiki_slug}/{slug}.md`",
|
|
115
|
+
" - **Image agent** → tell it to read https://www.openalmanac.org/image-guidelines.md and find images for the draft at `~/.openalmanac/pages/{wiki_slug}/{slug}.md`",
|
|
116
|
+
" - **Linking agent** → tell it to read https://www.openalmanac.org/linking-guidelines.md and add wikilinks for the draft at `~/.openalmanac/pages/{wiki_slug}/{slug}.md` (dead links become stubs on publish)",
|
|
114
117
|
"",
|
|
115
|
-
"
|
|
116
|
-
" - **Fact-check agent** → tell it to read https://www.openalmanac.org/fact-checking-guidelines.md and fact-check the draft at `~/.openalmanac/articles/{wiki_slug}/{slug}.md`",
|
|
117
|
-
" - **Image agent** → tell it to read https://www.openalmanac.org/image-guidelines.md and find images for the draft at `~/.openalmanac/articles/{wiki_slug}/{slug}.md`",
|
|
118
|
-
" - **Linking agent** → tell it to read https://www.openalmanac.org/linking-guidelines.md and add wikilinks for the draft at `~/.openalmanac/articles/{wiki_slug}/{slug}.md` (dead links become stubs on publish)",
|
|
118
|
+
"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
119
|
"",
|
|
120
|
-
"7. **
|
|
120
|
+
"7. **Publish** — Validate and publish (`publish` with `slugs` and `wiki_slug`). Put per-page change notes in frontmatter as `edit_summary`. Share the exact URL from the publish response when single-page. Use `list_pages` to verify coverage.",
|
|
121
121
|
"",
|
|
122
|
-
"
|
|
123
|
-
"",
|
|
124
|
-
"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
|
+
"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 page. Everything reads the draft.",
|
|
125
123
|
"",
|
|
126
124
|
"## Wikilink syntax",
|
|
127
125
|
"",
|
|
@@ -160,21 +158,26 @@ export function createServer() {
|
|
|
160
158
|
"3. **Brainstorm structure with the user.** Be conversational, not a form-filler. Propose a few natural topics the wiki might have (3-6 bullet points max), what the scope is, what the voice/angle is. Ask what resonates. Don't write prose yet.",
|
|
161
159
|
"4. **Create the wiki.** `create_wiki` with title + description. The server auto-scaffolds a `main-page` with default homepage directives.",
|
|
162
160
|
"5. **Edit the main page in place.** `download` the auto-created `main-page` and edit the file (don't `new` a fresh `main-page` — the server derives slug from title, so a new scaffold would get a different slug and you'd end up with two homepages).",
|
|
163
|
-
"6. **Seed the topic hierarchy.** `
|
|
161
|
+
"6. **Seed the topic hierarchy.** `create_topics` with the topics you agreed on.",
|
|
164
162
|
"7. **Wire navigation.** `update_wiki_settings` with a `nav` array. Each NavItem needs exactly one of `page` / `topic` / `link`. Use `auto: {enabled: true}` on topic NavItems to auto-populate children from the topic DAG.",
|
|
165
|
-
"8. **Seed a few stub pages or first
|
|
163
|
+
"8. **Seed a few stub pages or first pages.** Stubs are fine — scaffold-and-fill-later is a supported workflow.",
|
|
164
|
+
" For homepage-style timelines, keep stubs in stub surfaces (`::awaiting-composition` / `::stub-list`) rather than recent-entry timelines — front-page activity should reflect real pages, not auto-created unwritten stubs.",
|
|
166
165
|
"",
|
|
167
166
|
"The conversation drives the shape of the wiki. Don't over-engineer the topic hierarchy or the nav on turn one. Ship a small coherent starting shape and grow it with the user.",
|
|
168
167
|
"",
|
|
169
168
|
"## Technical workflow",
|
|
170
169
|
"",
|
|
171
|
-
"Reading and searching
|
|
170
|
+
"Reading and searching pages 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.",
|
|
172
171
|
"",
|
|
173
|
-
"Core flow: login (once) → `whoami` (confirm identity) → `list_wikis` or `
|
|
172
|
+
"Core flow: login (once) → `whoami` (confirm identity) → `list_wikis` or `search_pages` (what exists?) → `search_web` + `read_webpage` (research) → `new` (scaffold) or `download` (existing) → edit files under ~/.openalmanac/pages/{wiki_slug}/ → `publish`.",
|
|
174
173
|
"",
|
|
175
|
-
"After publishing, share the celebration URL when applicable. Use `
|
|
174
|
+
"After publishing, share the celebration URL when applicable. Use `list_pages` with `wiki_slug` to browse a wiki's pages.",
|
|
176
175
|
"",
|
|
177
176
|
"When working with tool results, write down any important information you might need later, as the original tool result may be cleared.",
|
|
177
|
+
"",
|
|
178
|
+
"## Batching writes",
|
|
179
|
+
"",
|
|
180
|
+
"Most write tools take arrays. Pass a single-element array for one item — there is no separate singular tool. Examples: `create_topics([{ title: \"X\" }])`, `delete_pages({ page_slugs: [\"foo\"] })`, `publish({ slugs: [\"bar\"] })`. Do not call a tool in a loop when an array argument exists.",
|
|
178
181
|
].join("\n"),
|
|
179
182
|
});
|
|
180
183
|
registerAuthTools(server);
|
package/dist/setup.js
CHANGED
|
@@ -5,58 +5,25 @@ import { fileURLToPath } from "url";
|
|
|
5
5
|
import { spawnSync } from "child_process";
|
|
6
6
|
import { performLogin } from "./login-core.js";
|
|
7
7
|
import { getAuthStatus } from "./auth.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"mcp__almanac__read_webpage",
|
|
25
|
-
"mcp__almanac__register_sources",
|
|
26
|
-
"mcp__almanac__search_images",
|
|
27
|
-
"mcp__almanac__view_images",
|
|
28
|
-
],
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
name: "Write & Publish",
|
|
32
|
-
description: "create article drafts and publish edits",
|
|
33
|
-
tools: [
|
|
34
|
-
"mcp__almanac__new",
|
|
35
|
-
"mcp__almanac__publish",
|
|
36
|
-
],
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
name: "Auth",
|
|
40
|
-
description: "login and logout",
|
|
41
|
-
tools: ["mcp__almanac__login", "mcp__almanac__logout"],
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
name: "Community",
|
|
45
|
-
description: "communities and posts",
|
|
46
|
-
tools: [
|
|
47
|
-
"mcp__almanac__search_communities",
|
|
48
|
-
"mcp__almanac__create_community",
|
|
49
|
-
"mcp__almanac__create_post",
|
|
50
|
-
],
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
name: "People",
|
|
54
|
-
description: "search people profiles",
|
|
55
|
-
tools: ["mcp__almanac__search_people"],
|
|
56
|
-
},
|
|
8
|
+
import { MCP_TOOL_GROUPS, toClaudePermissionName, } from "./tool-registry.js";
|
|
9
|
+
import { EXAMPLE_PROMPT } from "./onboarding-copy.js";
|
|
10
|
+
// MCP-side permission groups come from the shared tool registry so adding a
|
|
11
|
+
// new MCP tool can never silently leave it un-grouped — see
|
|
12
|
+
// `src/tool-registry.ts` for the contract and `test/tool-registry.test.ts`
|
|
13
|
+
// for the drift check that enforces it.
|
|
14
|
+
function mcpGroupToPermissionGroup(group) {
|
|
15
|
+
return {
|
|
16
|
+
name: group.name,
|
|
17
|
+
description: group.description,
|
|
18
|
+
tools: group.tools.map(toClaudePermissionName),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
// Built-in Claude Code tool groups — not MCP tools, so they stay defined
|
|
22
|
+
// here. The user opts into them in the same TUI checkbox screen.
|
|
23
|
+
const CLAUDE_BUILTIN_TOOL_GROUPS = [
|
|
57
24
|
{
|
|
58
25
|
name: "Local Files",
|
|
59
|
-
description: "read & edit
|
|
26
|
+
description: "read & edit pages in ~/.openalmanac",
|
|
60
27
|
tools: [
|
|
61
28
|
"Read(~/.openalmanac/**)",
|
|
62
29
|
"Write(~/.openalmanac/**)",
|
|
@@ -69,6 +36,10 @@ const TOOL_GROUPS = [
|
|
|
69
36
|
tools: ["WebSearch", "WebFetch"],
|
|
70
37
|
},
|
|
71
38
|
];
|
|
39
|
+
const TOOL_GROUPS = [
|
|
40
|
+
...MCP_TOOL_GROUPS.map(mcpGroupToPermissionGroup),
|
|
41
|
+
...CLAUDE_BUILTIN_TOOL_GROUPS,
|
|
42
|
+
];
|
|
72
43
|
const AGENTS = [
|
|
73
44
|
{ name: "Claude Code", supported: true },
|
|
74
45
|
{ name: "Codex", supported: false },
|
|
@@ -103,7 +74,7 @@ const LOGO_LINES = [
|
|
|
103
74
|
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255a\u2550\u255d \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255a\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
|
|
104
75
|
"\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u2550\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d",
|
|
105
76
|
];
|
|
106
|
-
function printBanner(subtitle = "Write and publish
|
|
77
|
+
function printBanner(subtitle = "Write and publish pages with your AI agent") {
|
|
107
78
|
process.stdout.write("\n");
|
|
108
79
|
for (let i = 0; i < LOGO_LINES.length; i++) {
|
|
109
80
|
process.stdout.write(`${GRADIENT[i]}${LOGO_LINES[i]}${RST}\n`);
|
|
@@ -113,7 +84,7 @@ function printBanner(subtitle = "Write and publish articles with your AI agent")
|
|
|
113
84
|
function renderHeader(mode = "default") {
|
|
114
85
|
printBanner(mode === "reddit"
|
|
115
86
|
? "Turn any subreddit into a published wiki"
|
|
116
|
-
: "Write and publish
|
|
87
|
+
: "Write and publish pages with your AI agent");
|
|
117
88
|
}
|
|
118
89
|
function printBadge() {
|
|
119
90
|
process.stdout.write(`\n ${ACCENT_BG} almanac ${RST}\n`);
|
|
@@ -1053,45 +1024,46 @@ function printResult(clientsLabel, loginResult, configured, alreadyConfigured, t
|
|
|
1053
1024
|
w("");
|
|
1054
1025
|
}
|
|
1055
1026
|
function getNextSteps(clientsLabel) {
|
|
1027
|
+
const exampleLine = `${BLUE}"${EXAMPLE_PROMPT}"${RST}`;
|
|
1056
1028
|
if (clientsLabel === "Claude Code") {
|
|
1057
1029
|
return [
|
|
1058
1030
|
`Type ${WHITE_BOLD}claude${RST} to start Claude Code`,
|
|
1059
|
-
`Say ${
|
|
1031
|
+
`Say ${exampleLine}`,
|
|
1060
1032
|
];
|
|
1061
1033
|
}
|
|
1062
1034
|
if (clientsLabel === "Codex") {
|
|
1063
1035
|
return [
|
|
1064
1036
|
`Type ${WHITE_BOLD}codex${RST} to start Codex`,
|
|
1065
|
-
`Ask ${
|
|
1037
|
+
`Ask ${exampleLine}`,
|
|
1066
1038
|
];
|
|
1067
1039
|
}
|
|
1068
1040
|
if (clientsLabel === "Cursor") {
|
|
1069
1041
|
return [
|
|
1070
1042
|
`Open ${WHITE_BOLD}Cursor${RST} in your project`,
|
|
1071
|
-
`Ask ${
|
|
1043
|
+
`Ask ${exampleLine}`,
|
|
1072
1044
|
];
|
|
1073
1045
|
}
|
|
1074
1046
|
if (clientsLabel === "OpenCode") {
|
|
1075
1047
|
return [
|
|
1076
1048
|
`Type ${WHITE_BOLD}opencode${RST} to start OpenCode`,
|
|
1077
|
-
`Ask ${
|
|
1049
|
+
`Ask ${exampleLine}`,
|
|
1078
1050
|
];
|
|
1079
1051
|
}
|
|
1080
1052
|
if (clientsLabel === "Windsurf") {
|
|
1081
1053
|
return [
|
|
1082
1054
|
`Open ${WHITE_BOLD}Windsurf${RST} in your project`,
|
|
1083
|
-
`Ask ${
|
|
1055
|
+
`Ask ${exampleLine}`,
|
|
1084
1056
|
];
|
|
1085
1057
|
}
|
|
1086
1058
|
if (clientsLabel === "Claude Desktop") {
|
|
1087
1059
|
return [
|
|
1088
1060
|
`Open ${WHITE_BOLD}Claude Desktop${RST}`,
|
|
1089
|
-
`Ask ${
|
|
1061
|
+
`Ask ${exampleLine}`,
|
|
1090
1062
|
];
|
|
1091
1063
|
}
|
|
1092
1064
|
return [
|
|
1093
1065
|
`Open one of your configured agents in this project`,
|
|
1094
|
-
`Ask ${
|
|
1066
|
+
`Ask ${exampleLine}`,
|
|
1095
1067
|
];
|
|
1096
1068
|
}
|
|
1097
1069
|
/* ── Entry point ────────────────────────────────────────────────── */
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const MCP_TOOL_NAMES: readonly ["search_pages", "search_topics", "list_pages", "download", "new", "publish", "read_page", "delete_pages", "list_topics", "update_topic", "create_topics", "list_wikis", "create_wiki", "get_wiki_settings", "update_wiki_settings", "join_wiki", "get_wiki_membership", "login", "logout", "whoami", "search_web", "read_webpage", "search_images", "view_images"];
|
|
2
|
+
export type McpToolName = (typeof MCP_TOOL_NAMES)[number];
|
|
3
|
+
export declare const INTERACTIVE_TOOL_NAMES: readonly string[];
|
|
4
|
+
export interface McpToolGroup {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
tools: readonly McpToolName[];
|
|
8
|
+
}
|
|
9
|
+
export declare const MCP_TOOL_GROUPS: readonly McpToolGroup[];
|
|
10
|
+
export declare function toClaudePermissionName(name: McpToolName): string;
|
|
11
|
+
export declare const MCP_TOOL_PERMISSION_NAMES: readonly string[];
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Single source of truth for the MCP tools the OpenAlmanac server exposes.
|
|
2
|
+
//
|
|
3
|
+
// This file is the contract that ties three otherwise-independent surfaces
|
|
4
|
+
// together:
|
|
5
|
+
//
|
|
6
|
+
// 1. tools/*.ts — the actual `server.addTool({ name: ... })`
|
|
7
|
+
// registrations.
|
|
8
|
+
// 2. setup.ts TOOL_GROUPS — the permission grouping shown in the
|
|
9
|
+
// `npx openalmanac setup` TUI, written into
|
|
10
|
+
// `~/.claude/settings.json` so a user's
|
|
11
|
+
// agent can call these tools without a
|
|
12
|
+
// per-call approval prompt.
|
|
13
|
+
// 3. gui/config.js — the Electron app's allow-list passed to
|
|
14
|
+
// the Claude Code SDK.
|
|
15
|
+
//
|
|
16
|
+
// Drift between these three surfaces was a real bug: the rename refactor
|
|
17
|
+
// added `read_page`, `list_wikis`, `create_wiki`, the topic tools, etc., but
|
|
18
|
+
// only (1) was updated. (2) was still grouping the pre-refactor tool set, so
|
|
19
|
+
// users who ran `setup` had to manually approve every wiki/topic call.
|
|
20
|
+
//
|
|
21
|
+
// The drift test in `test/tool-registry.test.ts` walks every register*Tools
|
|
22
|
+
// function with a fake server, collects the names actually registered, and
|
|
23
|
+
// asserts:
|
|
24
|
+
//
|
|
25
|
+
// - every registered name is in MCP_TOOL_NAMES
|
|
26
|
+
// - every name in MCP_TOOL_NAMES is actually registered
|
|
27
|
+
// - every non-INTERACTIVE name appears in exactly one MCP_TOOL_GROUPS entry
|
|
28
|
+
//
|
|
29
|
+
// Adding a new MCP tool means: register it in tools/*.ts, add its name here,
|
|
30
|
+
// and place it in a group below. Skipping any of those three breaks CI.
|
|
31
|
+
export const MCP_TOOL_NAMES = [
|
|
32
|
+
// Pages
|
|
33
|
+
"search_pages",
|
|
34
|
+
"search_topics",
|
|
35
|
+
"list_pages",
|
|
36
|
+
"download",
|
|
37
|
+
"new",
|
|
38
|
+
"publish",
|
|
39
|
+
"read_page",
|
|
40
|
+
"delete_pages",
|
|
41
|
+
// Topics
|
|
42
|
+
"list_topics",
|
|
43
|
+
"update_topic",
|
|
44
|
+
"create_topics",
|
|
45
|
+
// Wikis
|
|
46
|
+
"list_wikis",
|
|
47
|
+
"create_wiki",
|
|
48
|
+
"get_wiki_settings",
|
|
49
|
+
"update_wiki_settings",
|
|
50
|
+
"join_wiki",
|
|
51
|
+
"get_wiki_membership",
|
|
52
|
+
// Account
|
|
53
|
+
"login",
|
|
54
|
+
"logout",
|
|
55
|
+
"whoami",
|
|
56
|
+
// Research
|
|
57
|
+
"search_web",
|
|
58
|
+
"read_webpage",
|
|
59
|
+
"search_images",
|
|
60
|
+
"view_images",
|
|
61
|
+
];
|
|
62
|
+
// Tools intentionally excluded from the setup-time permission grant because
|
|
63
|
+
// they require interactive UI / per-call approval. The drift test allows these
|
|
64
|
+
// to be absent from MCP_TOOL_GROUPS — but they must still appear in
|
|
65
|
+
// MCP_TOOL_NAMES if they are actually registered.
|
|
66
|
+
//
|
|
67
|
+
// `register_sources` was a GUI citation-bubble handshake; it is currently
|
|
68
|
+
// commented out in tools/research.ts (REV-62) and therefore not in
|
|
69
|
+
// MCP_TOOL_NAMES. When it comes back, add it both there and here.
|
|
70
|
+
export const INTERACTIVE_TOOL_NAMES = [
|
|
71
|
+
"register_sources",
|
|
72
|
+
];
|
|
73
|
+
// Permission groupings shown in the setup TUI. Each group is a checkbox the
|
|
74
|
+
// user toggles; checked groups are written into `~/.claude/settings.json`
|
|
75
|
+
// `permissions.allow` so the agent can call them without per-call approval.
|
|
76
|
+
//
|
|
77
|
+
// Group boundaries are user-facing — they should match the user's mental
|
|
78
|
+
// model ("Search & Read", "Write & Publish") rather than the file the tool
|
|
79
|
+
// happens to live in. Every non-INTERACTIVE name in MCP_TOOL_NAMES must
|
|
80
|
+
// appear in exactly one group; the drift test enforces this.
|
|
81
|
+
export const MCP_TOOL_GROUPS = [
|
|
82
|
+
{
|
|
83
|
+
name: "Search & Read",
|
|
84
|
+
description: "search, read, download, and browse pages, topics, and wikis",
|
|
85
|
+
tools: [
|
|
86
|
+
"search_pages",
|
|
87
|
+
"search_topics",
|
|
88
|
+
"list_pages",
|
|
89
|
+
"list_topics",
|
|
90
|
+
"list_wikis",
|
|
91
|
+
"download",
|
|
92
|
+
"read_page",
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "Research",
|
|
97
|
+
description: "web search, read pages, find and view images",
|
|
98
|
+
tools: [
|
|
99
|
+
"search_web",
|
|
100
|
+
"read_webpage",
|
|
101
|
+
"search_images",
|
|
102
|
+
"view_images",
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "Write & Publish",
|
|
107
|
+
description: "create, edit, and publish pages and topics",
|
|
108
|
+
tools: [
|
|
109
|
+
"new",
|
|
110
|
+
"publish",
|
|
111
|
+
"delete_pages",
|
|
112
|
+
"create_topics",
|
|
113
|
+
"update_topic",
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "Wikis",
|
|
118
|
+
description: "create wikis, configure settings, manage membership",
|
|
119
|
+
tools: [
|
|
120
|
+
"create_wiki",
|
|
121
|
+
"get_wiki_settings",
|
|
122
|
+
"update_wiki_settings",
|
|
123
|
+
"join_wiki",
|
|
124
|
+
"get_wiki_membership",
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "Account",
|
|
129
|
+
description: "login, logout, identity",
|
|
130
|
+
tools: [
|
|
131
|
+
"login",
|
|
132
|
+
"logout",
|
|
133
|
+
"whoami",
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
// Convert a bare MCP tool name into the prefixed form Claude Code uses in
|
|
138
|
+
// `~/.claude/settings.json` permissions and the Electron SDK's allow-list.
|
|
139
|
+
//
|
|
140
|
+
// Example: `read_page` → `mcp__almanac__read_page`.
|
|
141
|
+
export function toClaudePermissionName(name) {
|
|
142
|
+
return `mcp__almanac__${name}`;
|
|
143
|
+
}
|
|
144
|
+
// Full set of allow-list entries for every registered MCP tool, in the
|
|
145
|
+
// `mcp__almanac__*` form Claude Code expects. Consumers (gui/config.js once
|
|
146
|
+
// it's bumped to a registry-aware mcp-ts version) should derive their tool
|
|
147
|
+
// allow-list from this.
|
|
148
|
+
export const MCP_TOOL_PERMISSION_NAMES = MCP_TOOL_NAMES.map(toClaudePermissionName);
|
package/dist/tools/auth.js
CHANGED
|
@@ -4,7 +4,7 @@ export function registerAuthTools(server) {
|
|
|
4
4
|
server.addTool({
|
|
5
5
|
name: "login",
|
|
6
6
|
description: "Log in via browser to connect your account and get a personal API key. This is the required " +
|
|
7
|
-
"first step before creating or updating
|
|
7
|
+
"first step before creating or updating pages. 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() {
|
|
10
10
|
const result = await performLogin();
|