@roam-research/roam-tools-core 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.
Files changed (44) hide show
  1. package/README.md +17 -0
  2. package/dist/client.d.ts +34 -0
  3. package/dist/client.d.ts.map +1 -0
  4. package/dist/client.js +275 -0
  5. package/dist/connect.d.ts +10 -0
  6. package/dist/connect.d.ts.map +1 -0
  7. package/dist/connect.js +477 -0
  8. package/dist/graph-resolver.d.ts +54 -0
  9. package/dist/graph-resolver.d.ts.map +1 -0
  10. package/dist/graph-resolver.js +338 -0
  11. package/dist/index.d.ts +9 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +10 -0
  14. package/dist/operations/blocks.d.ts +120 -0
  15. package/dist/operations/blocks.d.ts.map +1 -0
  16. package/dist/operations/blocks.js +108 -0
  17. package/dist/operations/files.d.ts +43 -0
  18. package/dist/operations/files.d.ts.map +1 -0
  19. package/dist/operations/files.js +175 -0
  20. package/dist/operations/graphs.d.ts +26 -0
  21. package/dist/operations/graphs.d.ts.map +1 -0
  22. package/dist/operations/graphs.js +214 -0
  23. package/dist/operations/navigation.d.ts +32 -0
  24. package/dist/operations/navigation.d.ts.map +1 -0
  25. package/dist/operations/navigation.js +54 -0
  26. package/dist/operations/pages.d.ts +63 -0
  27. package/dist/operations/pages.d.ts.map +1 -0
  28. package/dist/operations/pages.js +59 -0
  29. package/dist/operations/query.d.ts +34 -0
  30. package/dist/operations/query.d.ts.map +1 -0
  31. package/dist/operations/query.js +46 -0
  32. package/dist/operations/search.d.ts +37 -0
  33. package/dist/operations/search.d.ts.map +1 -0
  34. package/dist/operations/search.js +33 -0
  35. package/dist/roam-api.d.ts +32 -0
  36. package/dist/roam-api.d.ts.map +1 -0
  37. package/dist/roam-api.js +50 -0
  38. package/dist/tools.d.ts +22 -0
  39. package/dist/tools.d.ts.map +1 -0
  40. package/dist/tools.js +276 -0
  41. package/dist/types.d.ts +235 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +90 -0
  44. package/package.json +41 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/operations/query.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,aAAa,CAAC;AAKjE,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;EAStB,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,wBAAsB,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC,CA2B5F"}
@@ -0,0 +1,46 @@
1
+ import { z } from "zod";
2
+ import { textResult } from "../types.js";
3
+ // Schema for executing Roam queries ({{query: }} or {{[[query]]: }} blocks, NOT Datalog)
4
+ // Supports two modes: UID mode (execute existing query block) or Query mode (raw query string)
5
+ export const QuerySchema = z.object({
6
+ uid: z.string().optional().describe("UID of a block containing {{query: ...}} or {{[[query]]: ...}} - uses the block's saved display settings and filters"),
7
+ query: z.string().optional().describe("Raw Roam query string (e.g., \"{and: [[TODO]] {not: [[DONE]]}}\") - NOT Datalog - results are flat list, no user filters applied"),
8
+ sort: z.enum(["created-date", "edited-date", "daily-note-date"]).optional().describe("Sort order (only for query mode, default: created-date)"),
9
+ sortOrder: z.enum(["asc", "desc"]).optional().describe("Sort direction (only for query mode, default: desc)"),
10
+ includePath: z.boolean().optional().describe("Include breadcrumb path in results (only for query mode, default: true)"),
11
+ offset: z.coerce.number().optional().describe("Skip first N results (default: 0)"),
12
+ limit: z.coerce.number().optional().describe("Max results to return (default: 20)"),
13
+ maxDepth: z.coerce.number().optional().describe("Max depth of children to include in markdown (default: 1)"),
14
+ });
15
+ export async function query(client, params) {
16
+ // Validate: exactly one of uid or query must be provided
17
+ const hasUid = params.uid !== undefined;
18
+ const hasQuery = params.query !== undefined;
19
+ if (hasUid === hasQuery) {
20
+ throw new Error("Provide exactly one of 'uid' or 'query', not both or neither");
21
+ }
22
+ const apiParams = {};
23
+ if (params.uid !== undefined) {
24
+ // UID mode - execute existing query block
25
+ apiParams.uid = params.uid;
26
+ }
27
+ else {
28
+ // Query mode - raw query string
29
+ apiParams.query = params.query;
30
+ if (params.sort !== undefined)
31
+ apiParams.sort = params.sort;
32
+ if (params.sortOrder !== undefined)
33
+ apiParams.sortOrder = params.sortOrder;
34
+ if (params.includePath !== undefined)
35
+ apiParams.includePath = params.includePath;
36
+ }
37
+ // Common parameters for both modes
38
+ if (params.offset !== undefined)
39
+ apiParams.offset = params.offset;
40
+ if (params.limit !== undefined)
41
+ apiParams.limit = params.limit;
42
+ if (params.maxDepth !== undefined)
43
+ apiParams.maxDepth = params.maxDepth;
44
+ const response = await client.call("data.ai.roamQuery", [apiParams]);
45
+ return textResult(response.result ?? { total: 0, results: [] });
46
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ import type { RoamClient } from "../client.js";
3
+ import type { CallToolResult } from "../types.js";
4
+ export declare const SearchSchema: z.ZodObject<{
5
+ query: z.ZodString;
6
+ scope: z.ZodOptional<z.ZodEnum<["pages", "blocks", "all"]>>;
7
+ offset: z.ZodOptional<z.ZodNumber>;
8
+ limit: z.ZodOptional<z.ZodNumber>;
9
+ includePath: z.ZodOptional<z.ZodBoolean>;
10
+ maxDepth: z.ZodOptional<z.ZodNumber>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ query: string;
13
+ maxDepth?: number | undefined;
14
+ offset?: number | undefined;
15
+ limit?: number | undefined;
16
+ includePath?: boolean | undefined;
17
+ scope?: "pages" | "blocks" | "all" | undefined;
18
+ }, {
19
+ query: string;
20
+ maxDepth?: number | undefined;
21
+ offset?: number | undefined;
22
+ limit?: number | undefined;
23
+ includePath?: boolean | undefined;
24
+ scope?: "pages" | "blocks" | "all" | undefined;
25
+ }>;
26
+ export declare const SearchTemplatesSchema: z.ZodObject<{
27
+ query: z.ZodOptional<z.ZodString>;
28
+ }, "strip", z.ZodTypeAny, {
29
+ query?: string | undefined;
30
+ }, {
31
+ query?: string | undefined;
32
+ }>;
33
+ export type SearchParams = z.infer<typeof SearchSchema>;
34
+ export type SearchTemplatesParams = z.infer<typeof SearchTemplatesSchema>;
35
+ export declare function search(client: RoamClient, params: SearchParams): Promise<CallToolResult>;
36
+ export declare function searchTemplates(client: RoamClient, params: SearchTemplatesParams): Promise<CallToolResult>;
37
+ //# sourceMappingURL=search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/operations/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAA4B,cAAc,EAAE,MAAM,aAAa,CAAC;AAI5E,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;EAOvB,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;EAEhC,CAAC;AAGH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AACxD,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE1E,wBAAsB,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC,CAY9F;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAKzB"}
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ import { textResult } from "../types.js";
3
+ // Schemas
4
+ export const SearchSchema = z.object({
5
+ query: z.string().describe("Search query"),
6
+ scope: z.enum(["pages", "blocks", "all"]).optional().describe("Search scope: 'pages' for page titles only, 'blocks' for block content only, 'all' for both (default: 'all')"),
7
+ offset: z.coerce.number().optional().describe("Skip first N results (default: 0)"),
8
+ limit: z.coerce.number().optional().describe("Max results (default: 20)"),
9
+ includePath: z.boolean().optional().describe("Include breadcrumb path to each result (default: true)"),
10
+ maxDepth: z.coerce.number().optional().describe("Max depth of children to include in markdown (default: 0)"),
11
+ });
12
+ export const SearchTemplatesSchema = z.object({
13
+ query: z.string().optional().describe("Keywords to filter templates by name (case-insensitive). Try relevant keywords first before listing all."),
14
+ });
15
+ export async function search(client, params) {
16
+ const apiParams = {
17
+ query: params.query,
18
+ scope: params.scope ?? "all",
19
+ offset: params.offset ?? 0,
20
+ limit: params.limit ?? 20,
21
+ includePath: params.includePath ?? true,
22
+ };
23
+ if (params.maxDepth !== undefined)
24
+ apiParams.maxDepth = params.maxDepth;
25
+ const response = await client.call("data.ai.search", [apiParams]);
26
+ return textResult(response.result ?? { total: 0, results: [] });
27
+ }
28
+ export async function searchTemplates(client, params) {
29
+ const response = await client.call("data.ai.searchTemplates", [
30
+ { query: params.query },
31
+ ]);
32
+ return textResult(response.result ?? []);
33
+ }
@@ -0,0 +1,32 @@
1
+ import type { GraphType } from "./types.js";
2
+ export interface AvailableGraph {
3
+ name: string;
4
+ type: GraphType;
5
+ }
6
+ export interface GraphsResponse {
7
+ success: boolean;
8
+ result?: AvailableGraph[];
9
+ error?: string;
10
+ }
11
+ export interface TokenExchangeResponse {
12
+ success: boolean;
13
+ token?: string;
14
+ graphName?: string;
15
+ graphType?: GraphType;
16
+ grantedAccessLevel?: string;
17
+ grantedScopes?: {
18
+ read?: boolean;
19
+ append?: boolean;
20
+ edit?: boolean;
21
+ };
22
+ error?: {
23
+ code?: string;
24
+ message?: string;
25
+ } | string;
26
+ }
27
+ export declare function fetchAvailableGraphs(port: number): Promise<AvailableGraph[]>;
28
+ export declare function requestToken(port: number, graph: string, graphType: GraphType, accessLevel: string): Promise<TokenExchangeResponse>;
29
+ export declare function sleep(ms: number): Promise<void>;
30
+ export declare function openRoamApp(): Promise<void>;
31
+ export declare function slugify(input: string): string;
32
+ //# sourceMappingURL=roam-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roam-api.d.ts","sourceRoot":"","sources":["../src/roam-api.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAM5C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACrE,KAAK,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC;CACtD;AAMD,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,cAAc,EAAE,CAAC,CAc3B;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,SAAS,EACpB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,qBAAqB,CAAC,CAehC;AAMD,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAEjD;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C"}
@@ -0,0 +1,50 @@
1
+ // src/core/roam-api.ts
2
+ // Shared API functions for interacting with Roam's local API.
3
+ // Used by both the CLI (connect command) and the MCP tool (setup_new_graph).
4
+ import open from "open";
5
+ // ============================================================================
6
+ // API Functions
7
+ // ============================================================================
8
+ export async function fetchAvailableGraphs(port) {
9
+ const url = `http://127.0.0.1:${port}/api/graphs/available`;
10
+ const response = await fetch(url, {
11
+ method: "GET",
12
+ headers: { "Content-Type": "application/json" },
13
+ });
14
+ const data = (await response.json());
15
+ if (!data.success) {
16
+ throw new Error(data.error || "Failed to get available graphs");
17
+ }
18
+ return data.result || [];
19
+ }
20
+ export async function requestToken(port, graph, graphType, accessLevel) {
21
+ const url = `http://127.0.0.1:${port}/api/graphs/tokens/request`;
22
+ const response = await fetch(url, {
23
+ method: "POST",
24
+ headers: { "Content-Type": "application/json" },
25
+ body: JSON.stringify({
26
+ graph,
27
+ graphType,
28
+ description: "roam-mcp CLI",
29
+ accessLevel,
30
+ ai: true,
31
+ }),
32
+ });
33
+ return (await response.json());
34
+ }
35
+ // ============================================================================
36
+ // Helpers
37
+ // ============================================================================
38
+ export function sleep(ms) {
39
+ return new Promise((resolve) => setTimeout(resolve, ms));
40
+ }
41
+ export async function openRoamApp() {
42
+ await open("roam://open");
43
+ }
44
+ export function slugify(input) {
45
+ return input
46
+ .toLowerCase()
47
+ .replace(/[^a-z0-9]+/g, "-")
48
+ .replace(/^-|-$/g, "")
49
+ .replace(/-{2,}/g, "-");
50
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import type { CallToolResult } from "./types.js";
3
+ import { RoamClient } from "./client.js";
4
+ export interface ClientToolDefinition {
5
+ name: string;
6
+ description: string;
7
+ schema: z.ZodObject<z.ZodRawShape>;
8
+ action: (client: RoamClient, args: unknown) => Promise<CallToolResult>;
9
+ type: "client";
10
+ }
11
+ export interface StandaloneToolDefinition {
12
+ name: string;
13
+ description: string;
14
+ schema: z.ZodObject<z.ZodRawShape>;
15
+ action: (args: unknown) => Promise<CallToolResult>;
16
+ type: "standalone";
17
+ }
18
+ export type ToolDefinition = ClientToolDefinition | StandaloneToolDefinition;
19
+ export declare const tools: ToolDefinition[];
20
+ export declare function findTool(name: string): ToolDefinition | undefined;
21
+ export declare function routeToolCall(toolName: string, args: Record<string, unknown>): Promise<CallToolResult>;
22
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,cAAc,EAAkC,MAAM,YAAY,CAAC;AAEjF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA8BzC,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IACnC,MAAM,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IACvE,IAAI,EAAE,QAAQ,CAAC;CAChB;AAGD,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IACnC,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IACnD,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,MAAM,cAAc,GAAG,oBAAoB,GAAG,wBAAwB,CAAC;AAmL7E,eAAO,MAAM,KAAK,EAAE,cAAc,EAGjC,CAAC;AAEF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAEjE;AA0FD,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CAAC,cAAc,CAAC,CA+HzB"}
package/dist/tools.js ADDED
@@ -0,0 +1,276 @@
1
+ import { z } from "zod";
2
+ import { RoamError } from "./types.js";
3
+ import { RoamClient } from "./client.js";
4
+ import { resolveGraph, getPort, updateGraphTokenStatus } from "./graph-resolver.js";
5
+ import { CreatePageSchema, GetPageSchema, DeletePageSchema, UpdatePageSchema, GetGuidelinesSchema, createPage, getPage, deletePage, updatePage, getGuidelines, } from "./operations/pages.js";
6
+ import { CreateBlockSchema, GetBlockSchema, UpdateBlockSchema, DeleteBlockSchema, MoveBlockSchema, GetBacklinksSchema, createBlock, getBlock, updateBlock, deleteBlock, moveBlock, getBacklinks, } from "./operations/blocks.js";
7
+ import { SearchSchema, SearchTemplatesSchema, search, searchTemplates } from "./operations/search.js";
8
+ import { QuerySchema, query } from "./operations/query.js";
9
+ import { GetOpenWindowsSchema, GetSelectionSchema, OpenMainWindowSchema, OpenSidebarSchema, getOpenWindows, getSelection, openMainWindow, openSidebar, } from "./operations/navigation.js";
10
+ import { FileGetSchema, FileUploadSchema, FileDeleteSchema, getFile, uploadFile, deleteFile } from "./operations/files.js";
11
+ import { ListGraphsSchema, SetupNewGraphSchema, listGraphs, setupNewGraph } from "./operations/graphs.js";
12
+ // Common schema for graph parameter (used by most tools)
13
+ const GraphSchema = z.object({
14
+ graph: z.string().optional().describe("Graph nickname or name (optional - auto-selects if only one graph is configured)"),
15
+ });
16
+ // Helper to extend any schema with graph parameter
17
+ function withGraph(schema) {
18
+ return schema.extend(GraphSchema.shape);
19
+ }
20
+ // Helper to create tool with graph parameter
21
+ function defineTool(name, description, schema, action) {
22
+ return {
23
+ name,
24
+ description,
25
+ schema: withGraph(schema),
26
+ action: (client, args) => action(client, args),
27
+ type: "client",
28
+ };
29
+ }
30
+ // Helper to create standalone tool (no graph parameter, handles its own resolution)
31
+ function defineStandaloneTool(name, description, schema, action) {
32
+ return {
33
+ name,
34
+ description,
35
+ schema: schema,
36
+ action: (args) => action(args),
37
+ type: "standalone",
38
+ };
39
+ }
40
+ // Graph Management Tools (standalone - handle their own resolution)
41
+ const graphManagementTools = [
42
+ defineStandaloneTool("list_graphs", "List all configured graphs with their nicknames. Also provides setup instructions for connecting additional graphs.", ListGraphsSchema, listGraphs),
43
+ defineStandaloneTool("setup_new_graph", "Set up a new Roam graph for access, or list available graphs. Call without arguments to see which graphs are available in Roam Desktop. Call with graph and nickname to connect a specific graph — ask the user what they'd like to call the graph before choosing a nickname. The user will see an approval dialog in Roam desktop app and must approve the token request. If the graph is already configured, returns the existing configuration without making changes.", SetupNewGraphSchema, setupNewGraph),
44
+ ];
45
+ // Note appended to all client tool descriptions
46
+ const GUIDELINES_NOTE = "\n\nNote: Call get_graph_guidelines first when starting to work with a graph.";
47
+ // Content Tools (require graph/client)
48
+ const contentTools = [
49
+ defineTool("get_graph_guidelines", "IMPORTANT: Call this tool first when starting to work with a graph, before performing any other operations. Returns user-defined instructions and preferences for AI agents. The user may have specified naming conventions, preferred structures, or constraints that should guide your behavior.", GetGuidelinesSchema, getGuidelines),
50
+ defineTool("create_page", "Create a new page in Roam, optionally with markdown content." + GUIDELINES_NOTE, CreatePageSchema, createPage),
51
+ defineTool("create_block", "Create a new block under a parent, using markdown content. Supports nested bulleted lists - pass a markdown string with `- ` list items and indentation to create an entire block hierarchy in a single call." + GUIDELINES_NOTE, CreateBlockSchema, createBlock),
52
+ defineTool("update_block", "Update an existing block's content or properties." + GUIDELINES_NOTE, UpdateBlockSchema, updateBlock),
53
+ defineTool("delete_block", "Delete a block and all its children." + GUIDELINES_NOTE, DeleteBlockSchema, deleteBlock),
54
+ defineTool("move_block", "Move a block to a new location." + GUIDELINES_NOTE, MoveBlockSchema, moveBlock),
55
+ defineTool("delete_page", "Delete a page and all its contents." + GUIDELINES_NOTE, DeletePageSchema, deletePage),
56
+ defineTool("update_page", "Update a page's title or children view type. Set mergePages to true if renaming to a title that already exists." + GUIDELINES_NOTE, UpdatePageSchema, updatePage),
57
+ defineTool("search", "Search for pages and blocks by text. Returns paginated results with markdown content and optional breadcrumb paths." + GUIDELINES_NOTE, SearchSchema, search),
58
+ defineTool("search_templates", "Search Roam templates by name. When the user mentions 'my X template' or 'the X template', use this tool to find it. Templates are user-created reusable content blocks tagged with [[roam/templates]]. Returns template name, uid, and content as markdown." + GUIDELINES_NOTE, SearchTemplatesSchema, searchTemplates),
59
+ defineTool("roam_query", 'Execute a Roam query ({{query: }} or {{[[query]]: }} blocks, NOT Datalog). Two modes: (1) UID mode - pass a block UID containing a query component to run it with saved settings/filters; (2) Query mode - pass a raw query string like "{and: [[TODO]] {not: [[DONE]]}}". Returns paginated results with markdown content.' + GUIDELINES_NOTE, QuerySchema, query),
60
+ defineTool("get_page", "Get a page's content as markdown. Returns content with <roam> metadata tags containing UIDs - use these for follow-up operations but strip them when showing content to the user. Show remaining content verbatim, never paraphrase. Use maxDepth for large pages." + GUIDELINES_NOTE, GetPageSchema, getPage),
61
+ defineTool("get_block", "Get a block's content as markdown. Returns content with <roam> metadata tags containing UIDs - use these for follow-up operations but strip them when showing content to the user. Show remaining content verbatim, never paraphrase. Use maxDepth for large blocks." + GUIDELINES_NOTE, GetBlockSchema, getBlock),
62
+ defineTool("get_backlinks", "Get paginated backlinks (linked references) for a page or block, formatted as markdown. Returns total count and results with optional breadcrumb paths." + GUIDELINES_NOTE, GetBacklinksSchema, getBacklinks),
63
+ defineTool("get_open_windows", "Get the current view in the main window and all open sidebar windows." + GUIDELINES_NOTE, GetOpenWindowsSchema, getOpenWindows),
64
+ defineTool("get_selection", "Get the currently focused block and any multi-selected blocks." + GUIDELINES_NOTE, GetSelectionSchema, getSelection),
65
+ defineTool("open_main_window", "Navigate to a page or block in the main window." + GUIDELINES_NOTE, OpenMainWindowSchema, openMainWindow),
66
+ defineTool("open_sidebar", "Open a page or block in the right sidebar." + GUIDELINES_NOTE, OpenSidebarSchema, openSidebar),
67
+ defineTool("file_get", "Fetch a file hosted on Roam (handles decryption for encrypted graphs)." + GUIDELINES_NOTE, FileGetSchema, getFile),
68
+ defineTool("file_upload", "Upload an image to Roam. Returns the Firebase storage URL. Usually you'll want to create a new block with the image as markdown: `![](url)`. Provide ONE of: filePath (preferred - local file, server reads directly), url (remote URL, server fetches), or base64 (raw data, fallback for sandboxed clients)." + GUIDELINES_NOTE, FileUploadSchema, uploadFile),
69
+ defineTool("file_delete", "Delete a file hosted on Roam." + GUIDELINES_NOTE, FileDeleteSchema, deleteFile),
70
+ ];
71
+ export const tools = [
72
+ ...graphManagementTools,
73
+ ...contentTools,
74
+ ];
75
+ export function findTool(name) {
76
+ return tools.find((t) => t.name === name);
77
+ }
78
+ /**
79
+ * Prepend graph nickname to a tool result.
80
+ */
81
+ function prependGraphInfo(result, nickname) {
82
+ const prefix = `Roam graph: ${nickname}`;
83
+ const content = result.content;
84
+ if (!content || content.length === 0)
85
+ return result;
86
+ const first = content[0];
87
+ if (first.type === "text") {
88
+ return {
89
+ ...result,
90
+ content: [
91
+ { ...first, text: `${prefix}\n\n${first.text}` },
92
+ ...content.slice(1),
93
+ ],
94
+ };
95
+ }
96
+ // For image or other content types, prepend a text block
97
+ return {
98
+ ...result,
99
+ content: [
100
+ { type: "text", text: prefix },
101
+ ...content,
102
+ ],
103
+ };
104
+ }
105
+ /**
106
+ * Enrich a JSON text result with token info (accessLevel + scopes).
107
+ */
108
+ function enrichResultWithTokenInfo(result, info) {
109
+ const first = result.content?.[0];
110
+ if (!first || first.type !== "text")
111
+ return result;
112
+ try {
113
+ const parsed = JSON.parse(first.text);
114
+ parsed.accessLevel = info.grantedAccessLevel;
115
+ parsed.scopes = info.grantedScopes;
116
+ return {
117
+ ...result,
118
+ content: [{ ...first, text: JSON.stringify(parsed, null, 2) }, ...result.content.slice(1)],
119
+ };
120
+ }
121
+ catch {
122
+ return result;
123
+ }
124
+ }
125
+ /**
126
+ * Prepend a token revocation warning to the result.
127
+ */
128
+ function enrichResultWithTokenStatus(result, nickname) {
129
+ const warning = `Roam graph: ${nickname}\n\n` +
130
+ `WARNING: The token for this graph has been revoked.\n` +
131
+ `Call setup_new_graph with this graph's name to request a new token.\n`;
132
+ const first = result.content?.[0];
133
+ if (first?.type === "text") {
134
+ return {
135
+ ...result,
136
+ content: [{ ...first, text: warning + "\n" + first.text }, ...result.content.slice(1)],
137
+ };
138
+ }
139
+ return {
140
+ ...result,
141
+ content: [{ type: "text", text: warning }, ...(result.content || [])],
142
+ };
143
+ }
144
+ /**
145
+ * Convert a RoamError into a structured error result with isError: true.
146
+ */
147
+ function roamErrorResult(error) {
148
+ const errorPayload = {
149
+ error: {
150
+ code: error.code,
151
+ message: error.message,
152
+ ...(error.context || {}),
153
+ },
154
+ };
155
+ return {
156
+ content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }],
157
+ isError: true,
158
+ };
159
+ }
160
+ export async function routeToolCall(toolName, args) {
161
+ const tool = findTool(toolName);
162
+ if (!tool) {
163
+ throw new Error(`Unknown tool: ${toolName}`);
164
+ }
165
+ // Validate and parse args with Zod
166
+ const parsed = tool.schema.safeParse(args);
167
+ if (!parsed.success) {
168
+ throw new Error(`Invalid arguments: ${parsed.error.message}`);
169
+ }
170
+ // Handle standalone tools (graph management)
171
+ if (tool.type === "standalone") {
172
+ try {
173
+ return await tool.action(parsed.data);
174
+ }
175
+ catch (error) {
176
+ if (error instanceof RoamError) {
177
+ return roamErrorResult(error);
178
+ }
179
+ throw error;
180
+ }
181
+ }
182
+ // Handle client tools (require graph resolution)
183
+ try {
184
+ // Extract graph from validated args and resolve it
185
+ const { graph, ...restArgs } = parsed.data;
186
+ const resolvedGraph = await resolveGraph(graph);
187
+ const port = await getPort();
188
+ // Create client with full config
189
+ const client = new RoamClient({
190
+ graphName: resolvedGraph.name,
191
+ graphType: resolvedGraph.type,
192
+ token: resolvedGraph.token,
193
+ port,
194
+ });
195
+ // Special handling for get_graph_guidelines: sync token info in parallel
196
+ if (tool.name === "get_graph_guidelines") {
197
+ const [actionSettled, tokenInfoSettled] = await Promise.allSettled([
198
+ tool.action(client, restArgs),
199
+ client.getTokenInfo(),
200
+ ]);
201
+ // getTokenInfo() never throws, so always fulfilled
202
+ const tokenInfoResult = tokenInfoSettled.status === "fulfilled"
203
+ ? tokenInfoSettled.value
204
+ : { status: "unknown" };
205
+ // Handle revoked token FIRST (before examining action result)
206
+ if (tokenInfoResult.status === "revoked") {
207
+ if (resolvedGraph.lastKnownTokenStatus !== "revoked") {
208
+ try {
209
+ await updateGraphTokenStatus(resolvedGraph.nickname, { lastKnownTokenStatus: "revoked" });
210
+ }
211
+ catch { }
212
+ }
213
+ const baseResult = actionSettled.status === "fulfilled"
214
+ ? actionSettled.value
215
+ : actionSettled.reason instanceof RoamError
216
+ ? roamErrorResult(actionSettled.reason)
217
+ : { content: [{ type: "text", text: String(actionSettled.reason) }], isError: true };
218
+ return enrichResultWithTokenStatus(baseResult, resolvedGraph.nickname);
219
+ }
220
+ // Not revoked — if action failed, propagate the original error
221
+ if (actionSettled.status === "rejected") {
222
+ throw actionSettled.reason;
223
+ }
224
+ const result = actionSettled.value;
225
+ if (tokenInfoResult.status === "active") {
226
+ const info = tokenInfoResult.info;
227
+ // Validate access level before writing to prevent config corruption
228
+ const validLevels = ["read-only", "read-append", "full"];
229
+ const level = validLevels.includes(info.grantedAccessLevel)
230
+ ? info.grantedAccessLevel
231
+ : undefined;
232
+ // Only write to config if something actually changed
233
+ const accessLevelChanged = level && resolvedGraph.accessLevel !== level;
234
+ const tokenStatusChanged = resolvedGraph.lastKnownTokenStatus !== "active";
235
+ if (accessLevelChanged || tokenStatusChanged) {
236
+ try {
237
+ await updateGraphTokenStatus(resolvedGraph.nickname, {
238
+ ...(accessLevelChanged ? { accessLevel: level } : {}),
239
+ lastKnownTokenStatus: "active",
240
+ });
241
+ }
242
+ catch { }
243
+ }
244
+ if (!result.isError) {
245
+ const enriched = enrichResultWithTokenInfo(result, info);
246
+ return prependGraphInfo(enriched, resolvedGraph.nickname);
247
+ }
248
+ return result;
249
+ }
250
+ // status === "unknown" — action succeeded, so token isn't revoked; clear stale status
251
+ if (resolvedGraph.lastKnownTokenStatus !== "active") {
252
+ try {
253
+ await updateGraphTokenStatus(resolvedGraph.nickname, { lastKnownTokenStatus: "active" });
254
+ }
255
+ catch { }
256
+ }
257
+ if (!result.isError) {
258
+ return prependGraphInfo(result, resolvedGraph.nickname);
259
+ }
260
+ return result;
261
+ }
262
+ // Normal flow for all other tools
263
+ const result = await tool.action(client, restArgs);
264
+ // Prepend graph info to successful responses
265
+ if (!result.isError) {
266
+ return prependGraphInfo(result, resolvedGraph.nickname);
267
+ }
268
+ return result;
269
+ }
270
+ catch (error) {
271
+ if (error instanceof RoamError) {
272
+ return roamErrorResult(error);
273
+ }
274
+ throw error;
275
+ }
276
+ }