openalmanac 0.2.16 → 0.2.18

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/cli.js CHANGED
@@ -1,8 +1,15 @@
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
5
  const command = process.argv[2];
5
- if (command === "login") {
6
+ if (command === "setup") {
7
+ runSetup().catch((e) => {
8
+ console.error(e instanceof Error ? e.message : e);
9
+ process.exit(1);
10
+ });
11
+ }
12
+ else if (command === "login") {
6
13
  runLogin().catch((e) => {
7
14
  console.error(e instanceof Error ? e.message : e);
8
15
  process.exit(1);
@@ -0,0 +1 @@
1
+ export declare function runSetup(): Promise<void>;
package/dist/setup.js ADDED
@@ -0,0 +1,231 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ const TOOL_GROUPS = [
5
+ {
6
+ name: "Search & Read",
7
+ description: "search articles, pull, view status",
8
+ tools: [
9
+ "mcp__almanac__search_articles",
10
+ "mcp__almanac__pull",
11
+ "mcp__almanac__status",
12
+ "mcp__almanac__requested_articles",
13
+ ],
14
+ },
15
+ {
16
+ name: "Research",
17
+ description: "web search, read pages, find images",
18
+ tools: [
19
+ "mcp__almanac__search_web",
20
+ "mcp__almanac__read_webpage",
21
+ "mcp__almanac__search_images",
22
+ "mcp__almanac__view_image",
23
+ ],
24
+ },
25
+ {
26
+ name: "Write & Publish",
27
+ description: "create articles, push edits, stubs",
28
+ tools: [
29
+ "mcp__almanac__new",
30
+ "mcp__almanac__push",
31
+ "mcp__almanac__create_stub",
32
+ ],
33
+ },
34
+ {
35
+ name: "Auth",
36
+ description: "login and logout",
37
+ tools: ["mcp__almanac__login", "mcp__almanac__logout"],
38
+ },
39
+ {
40
+ name: "Community",
41
+ description: "communities, posts, article linking",
42
+ tools: [
43
+ "mcp__almanac__search_communities",
44
+ "mcp__almanac__create_community",
45
+ "mcp__almanac__create_post",
46
+ "mcp__almanac__link_article",
47
+ ],
48
+ },
49
+ {
50
+ name: "People",
51
+ description: "search people profiles",
52
+ tools: ["mcp__almanac__search_people"],
53
+ },
54
+ {
55
+ name: "Local Files",
56
+ description: "read & edit articles in ~/.openalmanac",
57
+ tools: [
58
+ "Read(~/.openalmanac/**)",
59
+ "Write(~/.openalmanac/**)",
60
+ "Edit(~/.openalmanac/**)",
61
+ ],
62
+ },
63
+ ];
64
+ /* ── ANSI helpers ───────────────────────────────────────────────── */
65
+ const RST = "\x1b[0m";
66
+ const BOLD = "\x1b[1m";
67
+ const DIM = "\x1b[2m";
68
+ const GREEN = "\x1b[32m";
69
+ const CYAN = "\x1b[36m";
70
+ const RED = "\x1b[31m";
71
+ const YELLOW = "\x1b[33m";
72
+ const MAGENTA = "\x1b[35m";
73
+ const WHITE_BOLD = "\x1b[1;37m";
74
+ /* ── File helpers ───────────────────────────────────────────────── */
75
+ const CLAUDE_DIR = join(homedir(), ".claude");
76
+ const MCP_JSON = join(CLAUDE_DIR, "mcp.json");
77
+ const SETTINGS_JSON = join(CLAUDE_DIR, "settings.json");
78
+ function ensureDir(dir) {
79
+ if (!existsSync(dir))
80
+ mkdirSync(dir, { recursive: true });
81
+ }
82
+ function readJson(path) {
83
+ try {
84
+ return JSON.parse(readFileSync(path, "utf-8"));
85
+ }
86
+ catch {
87
+ return {};
88
+ }
89
+ }
90
+ function writeJson(path, data) {
91
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
92
+ }
93
+ /* ── Step 1 — MCP server ───────────────────────────────────────── */
94
+ function configureMcp() {
95
+ ensureDir(CLAUDE_DIR);
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
+ const cfg = readJson(MCP_JSON);
98
+ if (!cfg.mcpServers)
99
+ cfg.mcpServers = {};
100
+ const cur = cfg.mcpServers.almanac;
101
+ if (cur?.command === "npx" &&
102
+ JSON.stringify(cur.args) === JSON.stringify(["-y", "openalmanac"])) {
103
+ return false; // already set
104
+ }
105
+ cfg.mcpServers.almanac = { command: "npx", args: ["-y", "openalmanac"] };
106
+ writeJson(MCP_JSON, cfg);
107
+ return true;
108
+ }
109
+ /* ── Step 2 — Permissions ──────────────────────────────────────── */
110
+ function configurePermissions(tools) {
111
+ ensureDir(CLAUDE_DIR);
112
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
+ const settings = readJson(SETTINGS_JSON);
114
+ if (!settings.permissions)
115
+ settings.permissions = {};
116
+ if (!Array.isArray(settings.permissions.allow))
117
+ settings.permissions.allow = [];
118
+ const existing = new Set(settings.permissions.allow);
119
+ let added = 0;
120
+ for (const t of tools) {
121
+ if (!existing.has(t)) {
122
+ settings.permissions.allow.push(t);
123
+ added++;
124
+ }
125
+ }
126
+ if (added > 0)
127
+ writeJson(SETTINGS_JSON, settings);
128
+ return tools.length;
129
+ }
130
+ /* ── TUI ────────────────────────────────────────────────────────── */
131
+ const MAX_NAME = Math.max(...TOOL_GROUPS.map((g) => g.name.length));
132
+ function render(selected, cursor, mcpLine) {
133
+ process.stdout.write("\x1b[2J\x1b[H"); // clear + home
134
+ const w = (s) => process.stdout.write(s + "\n");
135
+ w("");
136
+ w(` ${WHITE_BOLD}OpenAlmanac${RST} — Claude Code Setup`);
137
+ w(` ${DIM}${"─".repeat(36)}${RST}`);
138
+ w("");
139
+ w(` ${mcpLine}`);
140
+ w("");
141
+ w(` Allow all tools for a ${WHITE_BOLD}seamless${RST} writing experience?`);
142
+ w(` ${DIM}Deselect any you'd rather approve manually.${RST}`);
143
+ w("");
144
+ for (let i = 0; i < TOOL_GROUPS.length; i++) {
145
+ const arrow = i === cursor ? `${CYAN}❯${RST}` : " ";
146
+ const check = selected[i] ? `${GREEN}✓${RST}` : " ";
147
+ const pad = TOOL_GROUPS[i].name.padEnd(MAX_NAME + 2);
148
+ const name = i === cursor ? `${BOLD}${pad}${RST}` : pad;
149
+ const desc = `${DIM}${TOOL_GROUPS[i].description}${RST}`;
150
+ w(` ${arrow} [${check}] ${name} ${desc}`);
151
+ }
152
+ w("");
153
+ w(` ${GREEN}${BOLD}[enter]${RST} confirm ${CYAN}${BOLD}[space]${RST} toggle ${CYAN}${BOLD}[↑↓]${RST} move ${CYAN}${BOLD}[a]${RST} all ${CYAN}${BOLD}[q]${RST} quit`);
154
+ w("");
155
+ }
156
+ function runTui(mcpLine) {
157
+ return new Promise((resolve) => {
158
+ const selected = TOOL_GROUPS.map(() => true); // all on by default
159
+ let cursor = 0;
160
+ render(selected, cursor, mcpLine);
161
+ process.stdin.setRawMode(true);
162
+ process.stdin.resume();
163
+ process.stdin.setEncoding("utf-8");
164
+ const cleanup = () => {
165
+ process.stdin.removeListener("data", onData);
166
+ process.stdin.setRawMode(false);
167
+ process.stdin.pause();
168
+ };
169
+ const onData = (key) => {
170
+ // Ctrl-C / q → cancel
171
+ if (key === "\x03" || key === "q") {
172
+ cleanup();
173
+ process.stdout.write("\x1b[2J\x1b[H");
174
+ console.log("\n Setup cancelled.\n");
175
+ process.exit(0);
176
+ }
177
+ if (key === "\x1b[A" || key === "k")
178
+ cursor = (cursor - 1 + TOOL_GROUPS.length) % TOOL_GROUPS.length;
179
+ else if (key === "\x1b[B" || key === "j")
180
+ cursor = (cursor + 1) % TOOL_GROUPS.length;
181
+ else if (key === " ")
182
+ selected[cursor] = !selected[cursor];
183
+ else if (key === "a") {
184
+ const all = selected.every(Boolean);
185
+ selected.fill(!all);
186
+ }
187
+ else if (key === "\r" || key === "\n") {
188
+ cleanup();
189
+ const tools = [];
190
+ for (let i = 0; i < TOOL_GROUPS.length; i++) {
191
+ if (selected[i])
192
+ tools.push(...TOOL_GROUPS[i].tools);
193
+ }
194
+ resolve(tools);
195
+ return;
196
+ }
197
+ render(selected, cursor, mcpLine);
198
+ };
199
+ process.stdin.on("data", onData);
200
+ });
201
+ }
202
+ /* ── Result screen ──────────────────────────────────────────────── */
203
+ function printResult(mcpChanged, toolCount) {
204
+ process.stdout.write("\x1b[2J\x1b[H");
205
+ console.log("");
206
+ console.log(` ${WHITE_BOLD}OpenAlmanac${RST} — Claude Code Setup`);
207
+ console.log(` ${DIM}${"─".repeat(36)}${RST}`);
208
+ console.log("");
209
+ console.log(` ${GREEN}✓${RST} MCP server ${mcpChanged ? "added to" : "already in"} ${DIM}~/.claude/mcp.json${RST}`);
210
+ console.log(` ${GREEN}✓${RST} ${CYAN}${toolCount}${RST} tool${toolCount !== 1 ? "s" : ""} allowed in ${DIM}~/.claude/settings.json${RST}`);
211
+ console.log("");
212
+ console.log(` ${YELLOW}Restart Claude Code${RST} to apply changes.`);
213
+ console.log(` Then try: ${MAGENTA}"Write an article about quantum computing"${RST}`);
214
+ console.log("");
215
+ }
216
+ /* ── Entry point ────────────────────────────────────────────────── */
217
+ export async function runSetup() {
218
+ const skipTui = process.argv.includes("--yes") || process.argv.includes("-y");
219
+ const interactive = process.stdin.isTTY && !skipTui;
220
+ const mcpChanged = configureMcp();
221
+ const mcpLine = `${GREEN}✓${RST} MCP server ${mcpChanged ? "added" : "already configured"}`;
222
+ let tools;
223
+ if (interactive) {
224
+ tools = await runTui(mcpLine);
225
+ }
226
+ else {
227
+ tools = TOOL_GROUPS.flatMap((g) => g.tools);
228
+ }
229
+ const count = configurePermissions(tools);
230
+ printResult(mcpChanged, count);
231
+ }
@@ -87,15 +87,20 @@ export function registerResearchTools(server) {
87
87
  description: "View an image to verify it's suitable for an article. Use after search_images to inspect candidate images " +
88
88
  "before including them. Returns the image so you can see what it actually shows and write an accurate caption.",
89
89
  parameters: z.object({
90
- url: z.string().describe("Image URL to view (use image_url or thumbnail_url from search_images results)"),
90
+ url: z.string().url().describe("Image URL to view (use image_url or thumbnail_url from search_images results)"),
91
91
  }),
92
92
  async execute({ url }) {
93
- return {
94
- content: [
95
- await imageContent({ url }),
96
- { type: "text", text: `Image URL: ${url}` },
97
- ],
98
- };
93
+ try {
94
+ return {
95
+ content: [
96
+ await imageContent({ url }),
97
+ { type: "text", text: `Image URL: ${url}` },
98
+ ],
99
+ };
100
+ }
101
+ catch {
102
+ return `Failed to fetch image from ${url}. The URL may be invalid, inaccessible, or not an image.`;
103
+ }
99
104
  },
100
105
  });
101
106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openalmanac",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "description": "OpenAlmanac — pull, edit, and push articles to the open knowledge base",
5
5
  "type": "module",
6
6
  "bin": {