openalmanac 0.4.1 → 0.4.3
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 +1 -0
- package/dist/auth.js +14 -0
- package/dist/instructions.js +3 -3
- package/dist/onboarding-copy.js +1 -1
- package/dist/server.js +6 -4
- package/dist/setup/clients.js +1 -1
- package/dist/setup/tui.js +53 -32
- package/dist/tool-registry.js +1 -1
- package/dist/tool-tracking.d.ts +27 -0
- package/dist/tool-tracking.js +92 -0
- package/dist/tools/auth.js +1 -1
- package/dist/tools/pages/index.js +10 -10
- package/dist/tools/topics.js +2 -1
- package/dist/tools/wikis.js +19 -6
- package/package.json +1 -1
- package/dist/openalmanac_mcp-0.3.1-py3-none-any.whl +0 -0
- package/dist/openalmanac_mcp-0.3.1.tar.gz +0 -0
- package/dist/openalmanac_mcp-0.3.2-py3-none-any.whl +0 -0
- package/dist/openalmanac_mcp-0.3.2.tar.gz +0 -0
package/dist/auth.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type AuthStatus = {
|
|
|
14
14
|
};
|
|
15
15
|
export declare function getAuthStatus(): Promise<AuthStatus>;
|
|
16
16
|
export declare function buildAuthHeaders(): Record<string, string>;
|
|
17
|
+
export declare function requireValidApiKey(): Promise<void>;
|
|
17
18
|
export declare function request(method: string, path: string, options?: {
|
|
18
19
|
auth?: boolean;
|
|
19
20
|
params?: Record<string, string | number | boolean>;
|
package/dist/auth.js
CHANGED
|
@@ -59,6 +59,20 @@ export async function getAuthStatus() {
|
|
|
59
59
|
export function buildAuthHeaders() {
|
|
60
60
|
return { Authorization: `Bearer ${requireApiKey()}` };
|
|
61
61
|
}
|
|
62
|
+
export async function requireValidApiKey() {
|
|
63
|
+
const key = requireApiKey();
|
|
64
|
+
const resp = await fetch(`${API_BASE}/api/users/me`, {
|
|
65
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
66
|
+
signal: AbortSignal.timeout(10_000),
|
|
67
|
+
});
|
|
68
|
+
if (resp.ok)
|
|
69
|
+
return;
|
|
70
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
71
|
+
throw new Error(`Authentication failed (${resp.status}). Your API key may be invalid or expired. Run 'login' to re-authenticate.`);
|
|
72
|
+
}
|
|
73
|
+
const text = await resp.text();
|
|
74
|
+
throw new Error(`${resp.status} ${resp.statusText}: ${text}`);
|
|
75
|
+
}
|
|
62
76
|
export async function request(method, path, options = {}) {
|
|
63
77
|
const { auth = false, params, json, body, contentType } = options;
|
|
64
78
|
let url = `${API_BASE}${path}`;
|
package/dist/instructions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const SERVER_INSTRUCTIONS = [
|
|
2
|
-
"
|
|
2
|
+
"Almanac is an open knowledge base — a Wikipedia anyone can read from and write to through an API. Pages are markdown files with YAML frontmatter, [@key] citation markers, and [[wikilinks]]. Content is organized into wikis, each with topics, pages, and navigation.",
|
|
3
3
|
"",
|
|
4
4
|
"## How this should feel",
|
|
5
5
|
"",
|
|
@@ -113,7 +113,7 @@ export const SERVER_INSTRUCTIONS = [
|
|
|
113
113
|
"",
|
|
114
114
|
"## Working across wikis",
|
|
115
115
|
"",
|
|
116
|
-
"
|
|
116
|
+
"Almanac is multi-wiki. Every page lives inside one wiki. Before writing or creating anything, ground yourself:",
|
|
117
117
|
"",
|
|
118
118
|
"- `whoami` → who is the user? Needed to address them and to reason about \"my wikis\".",
|
|
119
119
|
"- `list_wikis` → what wikis exist? Use this BEFORE `create_wiki` so you can suggest contributing to an existing wiki instead of spinning up a parallel one.",
|
|
@@ -136,7 +136,7 @@ export const SERVER_INSTRUCTIONS = [
|
|
|
136
136
|
"",
|
|
137
137
|
"## Technical workflow",
|
|
138
138
|
"",
|
|
139
|
-
"
|
|
139
|
+
"All Almanac MCP tools except `login` and `logout` require login. Login creates a personal API key linked to your user account, so MCP reads, research, and contributions are attributed to you.",
|
|
140
140
|
"",
|
|
141
141
|
"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`.",
|
|
142
142
|
"",
|
package/dist/onboarding-copy.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// post-login connected page rendered by `login-core.ts`.
|
|
3
3
|
//
|
|
4
4
|
// The example prompt is a starter question on purpose: the goal is to give
|
|
5
|
-
// new users an easy first message that teaches them what
|
|
5
|
+
// new users an easy first message that teaches them what Almanac can do
|
|
6
6
|
// instead of assuming they already know what to ask. If the example prompt
|
|
7
7
|
// changes, both surfaces (`setup.ts` next steps + `login-core.ts` terminal
|
|
8
8
|
// mock) update automatically because they consume this single constant.
|
package/dist/server.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerTopicTools } from "./tools/topics.js";
|
|
|
10
10
|
import { registerUserTools } from "./tools/users.js";
|
|
11
11
|
import { getApiKey } from "./auth.js";
|
|
12
12
|
import { SERVER_INSTRUCTIONS } from "./instructions.js";
|
|
13
|
+
import { installMcpToolTracking } from "./tool-tracking.js";
|
|
13
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
15
16
|
export function createServer() {
|
|
@@ -17,12 +18,12 @@ export function createServer() {
|
|
|
17
18
|
console.error(`
|
|
18
19
|
┌──────────────────────────────────────────────────┐
|
|
19
20
|
│ │
|
|
20
|
-
│ Welcome to
|
|
21
|
+
│ Welcome to Almanac │
|
|
21
22
|
│ │
|
|
22
23
|
│ Try asking your agent: │
|
|
23
24
|
│ │
|
|
24
|
-
│ → "Write an Almanac page about CORS"
|
|
25
|
-
│ → "Improve the Alan Turing page"
|
|
25
|
+
│ → "Write an Almanac page about CORS" │
|
|
26
|
+
│ → "Improve the Alan Turing page" │
|
|
26
27
|
│ │
|
|
27
28
|
│ Docs: openalmanac.org/contribute │
|
|
28
29
|
│ │
|
|
@@ -30,10 +31,11 @@ export function createServer() {
|
|
|
30
31
|
`);
|
|
31
32
|
}
|
|
32
33
|
const server = new FastMCP({
|
|
33
|
-
name: "
|
|
34
|
+
name: "Almanac",
|
|
34
35
|
version: pkg.version,
|
|
35
36
|
instructions: SERVER_INSTRUCTIONS,
|
|
36
37
|
});
|
|
38
|
+
installMcpToolTracking(server, pkg.version);
|
|
37
39
|
registerAuthTools(server);
|
|
38
40
|
registerPageTools(server);
|
|
39
41
|
registerResearchTools(server);
|
package/dist/setup/clients.js
CHANGED
|
@@ -254,7 +254,7 @@ export function applyClientSetup(clients, mode) {
|
|
|
254
254
|
return { configured, alreadyConfigured };
|
|
255
255
|
}
|
|
256
256
|
export function printSetupPlan(clients, options) {
|
|
257
|
-
const heading = options.dryRun ? "Dry run" : "
|
|
257
|
+
const heading = options.dryRun ? "Dry run" : "Almanac MCP setup";
|
|
258
258
|
process.stdout.write(`${heading}\n\n`);
|
|
259
259
|
if (clients.length === 0) {
|
|
260
260
|
process.stdout.write("No supported clients detected. Use --client <name> to force a target or --print to inspect supported snippets.\n");
|
package/dist/setup/tui.js
CHANGED
|
@@ -57,6 +57,54 @@ function stepActive(msg) {
|
|
|
57
57
|
// Strip ANSI codes to measure visible length
|
|
58
58
|
const vis = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
59
59
|
const w = (s) => process.stdout.write(s + "\n");
|
|
60
|
+
function getBoxInnerWidth(contents, minWidth = 62) {
|
|
61
|
+
const terminalWidth = process.stdout.columns ?? 80;
|
|
62
|
+
const available = Math.max(40, terminalWidth - 6);
|
|
63
|
+
const widest = contents.reduce((max, content) => Math.max(max, vis(content)), 0);
|
|
64
|
+
return Math.min(Math.max(minWidth, widest), available);
|
|
65
|
+
}
|
|
66
|
+
function boxRow(content, innerW) {
|
|
67
|
+
const padding = Math.max(0, innerW - vis(content));
|
|
68
|
+
return ` ${BLUE_DIM}\u2502${RST}${content}${" ".repeat(padding)}${BLUE_DIM}\u2502${RST}`;
|
|
69
|
+
}
|
|
70
|
+
function wrapWithPrefixes(text, firstPrefix, nextPrefix, innerW) {
|
|
71
|
+
const words = text.split(" ").filter(Boolean);
|
|
72
|
+
if (words.length === 0)
|
|
73
|
+
return [firstPrefix];
|
|
74
|
+
const lines = [];
|
|
75
|
+
let prefix = firstPrefix;
|
|
76
|
+
let line = prefix;
|
|
77
|
+
let hasWord = false;
|
|
78
|
+
for (const word of words) {
|
|
79
|
+
const candidate = hasWord ? `${line} ${word}` : `${prefix}${word}`;
|
|
80
|
+
if (hasWord && vis(candidate) > innerW) {
|
|
81
|
+
lines.push(line);
|
|
82
|
+
prefix = nextPrefix;
|
|
83
|
+
line = `${prefix}${word}`;
|
|
84
|
+
hasWord = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
line = candidate;
|
|
88
|
+
hasWord = true;
|
|
89
|
+
}
|
|
90
|
+
lines.push(line);
|
|
91
|
+
return lines;
|
|
92
|
+
}
|
|
93
|
+
function renderNextStepsBox(lines) {
|
|
94
|
+
const header = ` ${WHITE_BOLD}Next steps${RST}`;
|
|
95
|
+
const innerW = getBoxInnerWidth([header, ...lines]);
|
|
96
|
+
const empty = boxRow("", innerW);
|
|
97
|
+
w(` ${BLUE_DIM}\u256d${"─".repeat(innerW)}\u256e${RST}`);
|
|
98
|
+
w(empty);
|
|
99
|
+
w(boxRow(header, innerW));
|
|
100
|
+
w(empty);
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
w(boxRow(line, innerW));
|
|
103
|
+
}
|
|
104
|
+
w(empty);
|
|
105
|
+
w(` ${BLUE_DIM}\u2570${"─".repeat(innerW)}\u256f${RST}`);
|
|
106
|
+
w("");
|
|
107
|
+
}
|
|
60
108
|
function renderClientSelect(clients, selected, cursor, mode = "default") {
|
|
61
109
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
62
110
|
renderHeader(mode);
|
|
@@ -395,24 +443,9 @@ export function printResult(clientsLabel, loginResult, configured, alreadyConfig
|
|
|
395
443
|
w(BAR);
|
|
396
444
|
stepDone(`${BLUE}Setup complete${RST}`);
|
|
397
445
|
w("");
|
|
398
|
-
// Next steps box
|
|
399
|
-
const innerW = 62;
|
|
400
|
-
const row = (content) => {
|
|
401
|
-
const padding = Math.max(0, innerW - vis(content));
|
|
402
|
-
return ` ${BLUE_DIM}\u2502${RST}${content}${" ".repeat(padding)}${BLUE_DIM}\u2502${RST}`;
|
|
403
|
-
};
|
|
404
|
-
const empty = row("");
|
|
405
446
|
const nextSteps = getNextSteps(clientsLabel);
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
w(row(` ${WHITE_BOLD}Next steps${RST}`));
|
|
409
|
-
w(empty);
|
|
410
|
-
for (let i = 0; i < nextSteps.length; i++) {
|
|
411
|
-
w(row(` ${BLUE}${i + 1}.${RST} ${nextSteps[i]}`));
|
|
412
|
-
}
|
|
413
|
-
w(empty);
|
|
414
|
-
w(` ${BLUE_DIM}\u2570${"─".repeat(innerW)}\u256f${RST}`);
|
|
415
|
-
w("");
|
|
447
|
+
const nextStepLines = nextSteps.flatMap((step, i) => wrapWithPrefixes(step, ` ${BLUE}${i + 1}.${RST} `, " ", 62));
|
|
448
|
+
renderNextStepsBox(nextStepLines);
|
|
416
449
|
}
|
|
417
450
|
function getNextSteps(clientsLabel) {
|
|
418
451
|
const exampleLine = `${BLUE}"${EXAMPLE_PROMPT}"${RST}`;
|
|
@@ -477,20 +510,8 @@ export function printRedditResult(agent, loginResult, mcpChanged, toolCount) {
|
|
|
477
510
|
w(BAR);
|
|
478
511
|
stepDone(`${BLUE}Setup complete${RST}`);
|
|
479
512
|
w("");
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const padding = Math.max(0, innerW - vis(content));
|
|
484
|
-
return ` ${BLUE_DIM}\u2502${RST}${content}${" ".repeat(padding)}${BLUE_DIM}\u2502${RST}`;
|
|
485
|
-
};
|
|
486
|
-
const empty = row("");
|
|
487
|
-
w(` ${BLUE_DIM}\u256d${"─".repeat(innerW)}\u256e${RST}`);
|
|
488
|
-
w(empty);
|
|
489
|
-
w(row(` ${WHITE_BOLD}Next steps${RST}`));
|
|
490
|
-
w(empty);
|
|
491
|
-
w(row(` ${BLUE}1.${RST} Type ${WHITE_BOLD}claude${RST} to start Claude Code`));
|
|
492
|
-
w(empty);
|
|
493
|
-
w(` ${BLUE_DIM}\u2570${"─".repeat(innerW)}\u256f${RST}`);
|
|
494
|
-
w("");
|
|
513
|
+
renderNextStepsBox([
|
|
514
|
+
` ${BLUE}1.${RST} Type ${WHITE_BOLD}claude${RST} to start Claude Code`,
|
|
515
|
+
]);
|
|
495
516
|
}
|
|
496
517
|
/* ── Reddit entry point ────────────────────────────────────────── */
|
package/dist/tool-registry.js
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FastMCP } from "fastmcp";
|
|
2
|
+
type ToolGroup = "read" | "write" | "research" | "wiki_admin" | "account";
|
|
3
|
+
type IntentDomain = "auth" | "page_read" | "page_write" | "search" | "topic_management" | "web_research" | "wiki_management";
|
|
4
|
+
export interface McpToolCallEvent {
|
|
5
|
+
tool_name: string;
|
|
6
|
+
tool_group: ToolGroup;
|
|
7
|
+
intent_domain: IntentDomain;
|
|
8
|
+
success: boolean;
|
|
9
|
+
duration_ms: number;
|
|
10
|
+
mcp_version: string;
|
|
11
|
+
error_type?: string;
|
|
12
|
+
}
|
|
13
|
+
interface McpToolDefinition {
|
|
14
|
+
name: string;
|
|
15
|
+
execute?: (...args: any[]) => unknown;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
interface ToolTrackingDeps {
|
|
19
|
+
mcpVersion: string;
|
|
20
|
+
validateAuth?: () => Promise<void>;
|
|
21
|
+
trackToolCall?: (event: McpToolCallEvent) => Promise<void>;
|
|
22
|
+
now?: () => number;
|
|
23
|
+
}
|
|
24
|
+
export declare function postMcpToolCall(event: McpToolCallEvent): Promise<void>;
|
|
25
|
+
export declare function wrapMcpToolDefinition<T extends McpToolDefinition>(definition: T, deps: ToolTrackingDeps): T;
|
|
26
|
+
export declare function installMcpToolTracking(server: FastMCP, mcpVersion: string): FastMCP;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { request, requireValidApiKey } from "./auth.js";
|
|
2
|
+
const AUTH_EXEMPT_TOOLS = new Set(["login", "logout"]);
|
|
3
|
+
const TOOL_METADATA = {
|
|
4
|
+
search_pages: { tool_group: "read", intent_domain: "search" },
|
|
5
|
+
search_topics: { tool_group: "read", intent_domain: "search" },
|
|
6
|
+
list_pages: { tool_group: "read", intent_domain: "page_read" },
|
|
7
|
+
list_topics: { tool_group: "read", intent_domain: "page_read" },
|
|
8
|
+
list_wikis: { tool_group: "read", intent_domain: "wiki_management" },
|
|
9
|
+
download: { tool_group: "read", intent_domain: "page_read" },
|
|
10
|
+
read_page: { tool_group: "read", intent_domain: "page_read" },
|
|
11
|
+
search_web: { tool_group: "research", intent_domain: "web_research" },
|
|
12
|
+
read_webpage: { tool_group: "research", intent_domain: "web_research" },
|
|
13
|
+
search_images: { tool_group: "research", intent_domain: "web_research" },
|
|
14
|
+
view_images: { tool_group: "research", intent_domain: "web_research" },
|
|
15
|
+
new: { tool_group: "write", intent_domain: "page_write" },
|
|
16
|
+
publish: { tool_group: "write", intent_domain: "page_write" },
|
|
17
|
+
delete_pages: { tool_group: "write", intent_domain: "page_write" },
|
|
18
|
+
create_topics: { tool_group: "write", intent_domain: "topic_management" },
|
|
19
|
+
update_topic: { tool_group: "write", intent_domain: "topic_management" },
|
|
20
|
+
create_wiki: { tool_group: "wiki_admin", intent_domain: "wiki_management" },
|
|
21
|
+
get_wiki_settings: { tool_group: "wiki_admin", intent_domain: "wiki_management" },
|
|
22
|
+
update_wiki_settings: { tool_group: "wiki_admin", intent_domain: "wiki_management" },
|
|
23
|
+
join_wiki: { tool_group: "wiki_admin", intent_domain: "wiki_management" },
|
|
24
|
+
get_wiki_membership: { tool_group: "wiki_admin", intent_domain: "wiki_management" },
|
|
25
|
+
whoami: { tool_group: "account", intent_domain: "auth" },
|
|
26
|
+
};
|
|
27
|
+
function metadataForTool(toolName) {
|
|
28
|
+
return TOOL_METADATA[toolName] ?? { tool_group: "account", intent_domain: "auth" };
|
|
29
|
+
}
|
|
30
|
+
function errorType(error) {
|
|
31
|
+
if (error instanceof Error && error.name)
|
|
32
|
+
return error.name;
|
|
33
|
+
return typeof error;
|
|
34
|
+
}
|
|
35
|
+
export async function postMcpToolCall(event) {
|
|
36
|
+
await request("POST", "/api/mcp/tool-calls", {
|
|
37
|
+
auth: true,
|
|
38
|
+
json: event,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export function wrapMcpToolDefinition(definition, deps) {
|
|
42
|
+
if (!definition.execute || AUTH_EXEMPT_TOOLS.has(definition.name)) {
|
|
43
|
+
return definition;
|
|
44
|
+
}
|
|
45
|
+
const execute = definition.execute;
|
|
46
|
+
const validateAuth = deps.validateAuth ?? requireValidApiKey;
|
|
47
|
+
const trackToolCall = deps.trackToolCall ?? postMcpToolCall;
|
|
48
|
+
const now = deps.now ?? Date.now;
|
|
49
|
+
const metadata = metadataForTool(definition.name);
|
|
50
|
+
return {
|
|
51
|
+
...definition,
|
|
52
|
+
async execute(...args) {
|
|
53
|
+
await validateAuth();
|
|
54
|
+
const startedAt = now();
|
|
55
|
+
let success = false;
|
|
56
|
+
let caught;
|
|
57
|
+
try {
|
|
58
|
+
const result = await execute(...args);
|
|
59
|
+
success = true;
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
caught = error;
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
const event = {
|
|
68
|
+
tool_name: definition.name,
|
|
69
|
+
...metadata,
|
|
70
|
+
success,
|
|
71
|
+
duration_ms: Math.max(0, Math.round(now() - startedAt)),
|
|
72
|
+
mcp_version: deps.mcpVersion,
|
|
73
|
+
};
|
|
74
|
+
if (caught !== undefined) {
|
|
75
|
+
event.error_type = errorType(caught);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await trackToolCall(event);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Analytics should never change tool behavior.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function installMcpToolTracking(server, mcpVersion) {
|
|
88
|
+
const addTool = server.addTool.bind(server);
|
|
89
|
+
server.addTool =
|
|
90
|
+
(definition) => addTool(wrapMcpToolDefinition(definition, { mcpVersion }));
|
|
91
|
+
return server;
|
|
92
|
+
}
|
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
|
|
7
|
+
"first step before using Almanac MCP tools. 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();
|
|
@@ -10,8 +10,8 @@ import { WRITING_GUIDE } from "./writing-guide.js";
|
|
|
10
10
|
export function registerPageTools(server) {
|
|
11
11
|
server.addTool({
|
|
12
12
|
name: "search_pages",
|
|
13
|
-
description: "Search
|
|
14
|
-
"or discover content. Optional wiki filter to scope results.
|
|
13
|
+
description: "Search Almanac pages and stubs across all wikis. Use to check existence, find slugs for wikilinks, " +
|
|
14
|
+
"or discover content. Optional wiki filter to scope results. Requires login.",
|
|
15
15
|
parameters: z.object({
|
|
16
16
|
queries: coerceJson(z.array(z.string()).min(1).max(20)).describe("Search queries (1-20)"),
|
|
17
17
|
wiki: z.string().optional().describe("Filter to a specific wiki slug"),
|
|
@@ -28,7 +28,7 @@ export function registerPageTools(server) {
|
|
|
28
28
|
};
|
|
29
29
|
if (wiki)
|
|
30
30
|
body.wiki = wiki;
|
|
31
|
-
const resp = await request("POST", "/api/search/batch", { json: body });
|
|
31
|
+
const resp = await request("POST", "/api/search/batch", { auth: true, json: body });
|
|
32
32
|
const data = (await resp.json());
|
|
33
33
|
const byQuery = {};
|
|
34
34
|
for (const set of data.results) {
|
|
@@ -39,8 +39,8 @@ export function registerPageTools(server) {
|
|
|
39
39
|
});
|
|
40
40
|
server.addTool({
|
|
41
41
|
name: "search_topics",
|
|
42
|
-
description: "Search
|
|
43
|
-
"Optional wiki filter to scope results.
|
|
42
|
+
description: "Search Almanac topics across all wikis. Use to discover topic slugs and topic pages. " +
|
|
43
|
+
"Optional wiki filter to scope results. Requires login.",
|
|
44
44
|
parameters: z.object({
|
|
45
45
|
queries: coerceJson(z.array(z.string()).min(1).max(20)).describe("Search queries (1-20)"),
|
|
46
46
|
wiki: z.string().optional().describe("Filter to a specific wiki slug"),
|
|
@@ -54,7 +54,7 @@ export function registerPageTools(server) {
|
|
|
54
54
|
};
|
|
55
55
|
if (wiki)
|
|
56
56
|
params.wiki = wiki;
|
|
57
|
-
const resp = await request("GET", "/api/search", { params });
|
|
57
|
+
const resp = await request("GET", "/api/search", { auth: true, params });
|
|
58
58
|
results[q] = await resp.json();
|
|
59
59
|
}
|
|
60
60
|
return JSON.stringify(results, null, 2);
|
|
@@ -64,7 +64,7 @@ export function registerPageTools(server) {
|
|
|
64
64
|
name: "list_pages",
|
|
65
65
|
description: "Browse pages in a wiki. Structured listing, not fuzzy search. " +
|
|
66
66
|
"Use to see what exists, find stubs, or discover pages by topic. " +
|
|
67
|
-
"Each returned page includes topic objects with both slug and title.",
|
|
67
|
+
"Each returned page includes topic objects with both slug and title. Requires login.",
|
|
68
68
|
parameters: z.object({
|
|
69
69
|
wiki_slug: z.string().describe("Wiki slug"),
|
|
70
70
|
topic: z.string().optional().describe("Filter by topic slug"),
|
|
@@ -78,7 +78,7 @@ export function registerPageTools(server) {
|
|
|
78
78
|
params.topic = topic;
|
|
79
79
|
if (stubs_only)
|
|
80
80
|
params.stub = true;
|
|
81
|
-
const resp = await request("GET", `/api/w/${wiki_slug}/pages`, { params });
|
|
81
|
+
const resp = await request("GET", `/api/w/${wiki_slug}/pages`, { auth: true, params });
|
|
82
82
|
return JSON.stringify(await resp.json(), null, 2);
|
|
83
83
|
},
|
|
84
84
|
});
|
|
@@ -272,13 +272,13 @@ export function registerPageTools(server) {
|
|
|
272
272
|
description: "Read a single page by slug. Returns the full page JSON including content, topics, sources, and infobox. " +
|
|
273
273
|
"No side effects — use this to read a page without downloading it to disk or joining the wiki. " +
|
|
274
274
|
"For editing, use `download` instead (it writes local files and handles ref tokens). " +
|
|
275
|
-
"For discovery, use `search_pages` instead.
|
|
275
|
+
"For discovery, use `search_pages` instead. Requires login.",
|
|
276
276
|
parameters: z.object({
|
|
277
277
|
wiki_slug: z.string().describe("Wiki slug"),
|
|
278
278
|
page_slug: z.string().describe("Page slug"),
|
|
279
279
|
}),
|
|
280
280
|
async execute({ wiki_slug, page_slug }) {
|
|
281
|
-
const resp = await request("GET", `/api/w/${wiki_slug}/pages/${page_slug}
|
|
281
|
+
const resp = await request("GET", `/api/w/${wiki_slug}/pages/${page_slug}`, { auth: true });
|
|
282
282
|
return JSON.stringify(await resp.json(), null, 2);
|
|
283
283
|
},
|
|
284
284
|
});
|
package/dist/tools/topics.js
CHANGED
|
@@ -4,13 +4,14 @@ import { coerceJson } from "../utils.js";
|
|
|
4
4
|
export function registerTopicTools(server) {
|
|
5
5
|
server.addTool({
|
|
6
6
|
name: "list_topics",
|
|
7
|
-
description: "List topics in a wiki. Returns flat list or graph (nodes + edges).
|
|
7
|
+
description: "List topics in a wiki. Returns flat list or graph (nodes + edges). Requires login.",
|
|
8
8
|
parameters: z.object({
|
|
9
9
|
wiki_slug: z.string().describe("Wiki slug"),
|
|
10
10
|
format: z.enum(["flat", "graph"]).default("flat").describe("Response format"),
|
|
11
11
|
}),
|
|
12
12
|
async execute({ wiki_slug, format }) {
|
|
13
13
|
const resp = await request("GET", `/api/w/${wiki_slug}/topics`, {
|
|
14
|
+
auth: true,
|
|
14
15
|
params: { format },
|
|
15
16
|
});
|
|
16
17
|
return JSON.stringify(await resp.json(), null, 2);
|
package/dist/tools/wikis.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { request } from "../auth.js";
|
|
3
3
|
import { coerceJson } from "../utils.js";
|
|
4
|
+
const MAX_IMAGE_URL_LENGTH = 2048;
|
|
5
|
+
const httpImageUrlSchema = z.string()
|
|
6
|
+
.max(MAX_IMAGE_URL_LENGTH)
|
|
7
|
+
.refine((value) => !value.startsWith("data:") && !value.includes("base64,"), "Use an http(s) image URL, not inline image data")
|
|
8
|
+
.refine((value) => value.startsWith("http://") || value.startsWith("https://"), "Image URL must start with http:// or https://");
|
|
9
|
+
const coverImageUrlSchema = z.string()
|
|
10
|
+
.max(MAX_IMAGE_URL_LENGTH)
|
|
11
|
+
.refine((value) => value.startsWith("linear-gradient(") ||
|
|
12
|
+
value.startsWith("http://") ||
|
|
13
|
+
value.startsWith("https://"), "Cover image must be an http(s) URL or gallery gradient")
|
|
14
|
+
.refine((value) => value.startsWith("linear-gradient(") ||
|
|
15
|
+
(!value.startsWith("data:") && !value.includes("base64,")), "Use an http(s) image URL, not inline image data");
|
|
4
16
|
// Mirrors backend `NavItem` in src/schemas/wiki_settings_schemas.py. The
|
|
5
17
|
// refinement matches the `exactly_one_target` @model_validator there —
|
|
6
18
|
// agents get a clear error pre-flight instead of a 422 round-trip.
|
|
@@ -34,7 +46,7 @@ const navItemSchema = z.lazy(() => z.object({
|
|
|
34
46
|
const themeSchema = z.object({
|
|
35
47
|
accent_color: z.string().optional(),
|
|
36
48
|
name_font: z.string().optional(),
|
|
37
|
-
logo_url:
|
|
49
|
+
logo_url: httpImageUrlSchema.nullable().optional(),
|
|
38
50
|
cover_tint_intensity: z.number().optional(),
|
|
39
51
|
logo_tint_intensity: z.number().optional(),
|
|
40
52
|
cover_y_offset: z.number().optional(),
|
|
@@ -42,12 +54,13 @@ const themeSchema = z.object({
|
|
|
42
54
|
export function registerWikiTools(server) {
|
|
43
55
|
server.addTool({
|
|
44
56
|
name: "list_wikis",
|
|
45
|
-
description: "List every wiki on
|
|
57
|
+
description: "List every wiki on Almanac. Use before creating a new wiki so you can suggest contributing to an existing one. The global almanac has slug `global` and is excluded by default — pass `include_global: true` to include it. Requires login.",
|
|
46
58
|
parameters: z.object({
|
|
47
59
|
include_global: z.boolean().default(false).describe("Include the global almanac wiki in results"),
|
|
48
60
|
}),
|
|
49
61
|
async execute({ include_global }) {
|
|
50
62
|
const resp = await request("GET", "/api/wikis", {
|
|
63
|
+
auth: true,
|
|
51
64
|
params: { include_global },
|
|
52
65
|
});
|
|
53
66
|
return JSON.stringify(await resp.json(), null, 2);
|
|
@@ -71,23 +84,23 @@ export function registerWikiTools(server) {
|
|
|
71
84
|
});
|
|
72
85
|
server.addTool({
|
|
73
86
|
name: "get_wiki_settings",
|
|
74
|
-
description: "Read a wiki's details and settings (nav, cover, theme).
|
|
87
|
+
description: "Read a wiki's details and settings (nav, cover, theme). Requires login.",
|
|
75
88
|
parameters: z.object({
|
|
76
89
|
wiki_slug: z.string().describe("Wiki slug"),
|
|
77
90
|
}),
|
|
78
91
|
async execute({ wiki_slug }) {
|
|
79
|
-
const resp = await request("GET", `/api/w/${wiki_slug}
|
|
92
|
+
const resp = await request("GET", `/api/w/${wiki_slug}`, { auth: true });
|
|
80
93
|
return JSON.stringify(await resp.json(), null, 2);
|
|
81
94
|
},
|
|
82
95
|
});
|
|
83
96
|
server.addTool({
|
|
84
97
|
name: "update_wiki_settings",
|
|
85
|
-
description: "Update a wiki's settings. Pass any combination of `nav`, `cover_image_url`, and `theme` — omitted fields are preserved (the backend uses exclude_unset merge). For example, `{nav: [...]}` updates navigation only without touching theme or cover_image_url. Each NavItem must have exactly one of `page`, `topic`, or `link`. Use `auto` (only on topic items) to auto-populate children from the topic DAG. Requires moderator access.",
|
|
98
|
+
description: "Update a wiki's settings. Pass any combination of `nav`, `cover_image_url`, and `theme` — omitted fields are preserved (the backend uses exclude_unset merge). For example, `{nav: [...]}` updates navigation only without touching theme or cover_image_url. `cover_image_url` accepts http(s) image URLs or gallery gradients; `theme.logo_url` accepts http(s) image URLs. Do not pass inline base64/data image strings. Each NavItem must have exactly one of `page`, `topic`, or `link`. Use `auto` (only on topic items) to auto-populate children from the topic DAG. Requires moderator access.",
|
|
86
99
|
parameters: z.object({
|
|
87
100
|
wiki_slug: z.string().describe("Wiki slug"),
|
|
88
101
|
settings: coerceJson(z.object({
|
|
89
102
|
nav: z.array(navItemSchema).optional(),
|
|
90
|
-
cover_image_url:
|
|
103
|
+
cover_image_url: coverImageUrlSchema.nullable().optional(),
|
|
91
104
|
theme: themeSchema.optional(),
|
|
92
105
|
})).describe("Settings to update"),
|
|
93
106
|
}),
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|