@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.
- package/README.md +17 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +275 -0
- package/dist/connect.d.ts +10 -0
- package/dist/connect.d.ts.map +1 -0
- package/dist/connect.js +477 -0
- package/dist/graph-resolver.d.ts +54 -0
- package/dist/graph-resolver.d.ts.map +1 -0
- package/dist/graph-resolver.js +338 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/operations/blocks.d.ts +120 -0
- package/dist/operations/blocks.d.ts.map +1 -0
- package/dist/operations/blocks.js +108 -0
- package/dist/operations/files.d.ts +43 -0
- package/dist/operations/files.d.ts.map +1 -0
- package/dist/operations/files.js +175 -0
- package/dist/operations/graphs.d.ts +26 -0
- package/dist/operations/graphs.d.ts.map +1 -0
- package/dist/operations/graphs.js +214 -0
- package/dist/operations/navigation.d.ts +32 -0
- package/dist/operations/navigation.d.ts.map +1 -0
- package/dist/operations/navigation.js +54 -0
- package/dist/operations/pages.d.ts +63 -0
- package/dist/operations/pages.d.ts.map +1 -0
- package/dist/operations/pages.js +59 -0
- package/dist/operations/query.d.ts +34 -0
- package/dist/operations/query.d.ts.map +1 -0
- package/dist/operations/query.js +46 -0
- package/dist/operations/search.d.ts +37 -0
- package/dist/operations/search.d.ts.map +1 -0
- package/dist/operations/search.js +33 -0
- package/dist/roam-api.d.ts +32 -0
- package/dist/roam-api.d.ts.map +1 -0
- package/dist/roam-api.js +50 -0
- package/dist/tools.d.ts +22 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +276 -0
- package/dist/types.d.ts +235 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +90 -0
- package/package.json +41 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { basename, extname } from "path";
|
|
4
|
+
import { imageResult, textResult } from "../types.js";
|
|
5
|
+
// Schemas
|
|
6
|
+
export const FileGetSchema = z.object({
|
|
7
|
+
url: z.string().describe("Firebase storage URL of the file"),
|
|
8
|
+
});
|
|
9
|
+
export const FileUploadSchema = z.object({
|
|
10
|
+
filePath: z.string().optional().describe("Local file path (preferred) - server reads the file directly"),
|
|
11
|
+
url: z.string().optional().describe("Remote URL to fetch the image from"),
|
|
12
|
+
base64: z.string().optional().describe("Base64-encoded image data (fallback for sandboxed clients)"),
|
|
13
|
+
mimetype: z.string().optional().describe("MIME type (e.g., image/png, image/jpeg) - auto-detected if not provided"),
|
|
14
|
+
filename: z.string().optional().describe("Original filename for reference - derived from path/url if not provided"),
|
|
15
|
+
});
|
|
16
|
+
export const FileDeleteSchema = z.object({
|
|
17
|
+
url: z.string().describe("Firebase storage URL of the file to delete"),
|
|
18
|
+
});
|
|
19
|
+
// Detect MIME type from base64 image data by checking magic bytes
|
|
20
|
+
function detectMimeTypeFromBase64(base64) {
|
|
21
|
+
// JPEG: FF D8 FF -> /9j/
|
|
22
|
+
if (base64.startsWith("/9j/"))
|
|
23
|
+
return "image/jpeg";
|
|
24
|
+
// PNG: 89 50 4E 47 -> iVBOR
|
|
25
|
+
if (base64.startsWith("iVBOR"))
|
|
26
|
+
return "image/png";
|
|
27
|
+
// GIF: 47 49 46 38 -> R0lG
|
|
28
|
+
if (base64.startsWith("R0lG"))
|
|
29
|
+
return "image/gif";
|
|
30
|
+
// WebP: 52 49 46 46 ... 57 45 42 50 -> UklGR
|
|
31
|
+
if (base64.startsWith("UklGR"))
|
|
32
|
+
return "image/webp";
|
|
33
|
+
// BMP: 42 4D -> Qk
|
|
34
|
+
if (base64.startsWith("Qk"))
|
|
35
|
+
return "image/bmp";
|
|
36
|
+
// PDF: 25 50 44 46 -> JVBE
|
|
37
|
+
if (base64.startsWith("JVBE"))
|
|
38
|
+
return "application/pdf";
|
|
39
|
+
// TIFF (little-endian): 49 49 2A 00 -> SUkq
|
|
40
|
+
if (base64.startsWith("SUkq"))
|
|
41
|
+
return "image/tiff";
|
|
42
|
+
// TIFF (big-endian): 4D 4D 00 2A -> TU0A
|
|
43
|
+
if (base64.startsWith("TU0A"))
|
|
44
|
+
return "image/tiff";
|
|
45
|
+
// ICO: 00 00 01 00 -> AAAB
|
|
46
|
+
if (base64.startsWith("AAAB"))
|
|
47
|
+
return "image/x-icon";
|
|
48
|
+
// AVIF/HEIC: check for ftyp box (base64 varies, check common patterns)
|
|
49
|
+
// These are harder to detect reliably from base64 prefix alone
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
// Detect MIME type from file extension
|
|
53
|
+
function detectMimeTypeFromExtension(filePath) {
|
|
54
|
+
const ext = extname(filePath).toLowerCase();
|
|
55
|
+
const mimeTypes = {
|
|
56
|
+
// Common image formats
|
|
57
|
+
".jpg": "image/jpeg",
|
|
58
|
+
".jpeg": "image/jpeg",
|
|
59
|
+
".png": "image/png",
|
|
60
|
+
".gif": "image/gif",
|
|
61
|
+
".webp": "image/webp",
|
|
62
|
+
".bmp": "image/bmp",
|
|
63
|
+
".svg": "image/svg+xml",
|
|
64
|
+
".ico": "image/x-icon",
|
|
65
|
+
".tiff": "image/tiff",
|
|
66
|
+
".tif": "image/tiff",
|
|
67
|
+
// Modern formats
|
|
68
|
+
".heic": "image/heic",
|
|
69
|
+
".heif": "image/heif",
|
|
70
|
+
".avif": "image/avif",
|
|
71
|
+
// Documents
|
|
72
|
+
".pdf": "application/pdf",
|
|
73
|
+
};
|
|
74
|
+
return mimeTypes[ext] || null;
|
|
75
|
+
}
|
|
76
|
+
// Read file from local path and return base64 + mimetype
|
|
77
|
+
async function readLocalFile(filePath) {
|
|
78
|
+
const buffer = await readFile(filePath);
|
|
79
|
+
const base64 = buffer.toString("base64");
|
|
80
|
+
const filename = basename(filePath);
|
|
81
|
+
// Try extension first, then magic bytes
|
|
82
|
+
let mimetype = detectMimeTypeFromExtension(filePath);
|
|
83
|
+
if (!mimetype) {
|
|
84
|
+
mimetype = detectMimeTypeFromBase64(base64);
|
|
85
|
+
}
|
|
86
|
+
if (!mimetype) {
|
|
87
|
+
throw new Error(`Could not detect MIME type for file: ${filePath}`);
|
|
88
|
+
}
|
|
89
|
+
return { base64, mimetype, filename };
|
|
90
|
+
}
|
|
91
|
+
// Fetch file from URL and return base64 + mimetype
|
|
92
|
+
async function fetchRemoteFile(url) {
|
|
93
|
+
const response = await fetch(url);
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
96
|
+
}
|
|
97
|
+
const buffer = await response.arrayBuffer();
|
|
98
|
+
const base64 = Buffer.from(buffer).toString("base64");
|
|
99
|
+
// Extract filename from URL path
|
|
100
|
+
const urlPath = new URL(url).pathname;
|
|
101
|
+
const filename = basename(urlPath) || "image";
|
|
102
|
+
// Try Content-Type header first, then extension, then magic bytes
|
|
103
|
+
let mimetype = response.headers.get("content-type")?.split(";")[0];
|
|
104
|
+
if (!mimetype || mimetype === "application/octet-stream") {
|
|
105
|
+
mimetype = detectMimeTypeFromExtension(urlPath) || detectMimeTypeFromBase64(base64) || undefined;
|
|
106
|
+
}
|
|
107
|
+
if (!mimetype) {
|
|
108
|
+
throw new Error(`Could not detect MIME type for URL: ${url}`);
|
|
109
|
+
}
|
|
110
|
+
return { base64, mimetype, filename };
|
|
111
|
+
}
|
|
112
|
+
export async function getFile(client, params) {
|
|
113
|
+
const response = await client.call("file.get", [
|
|
114
|
+
{ url: params.url, format: "base64" },
|
|
115
|
+
]);
|
|
116
|
+
if (!response.result) {
|
|
117
|
+
throw new Error("No file data returned");
|
|
118
|
+
}
|
|
119
|
+
const { base64, mimetype } = response.result;
|
|
120
|
+
const mimeType = mimetype || detectMimeTypeFromBase64(base64);
|
|
121
|
+
if (mimeType?.startsWith("image/")) {
|
|
122
|
+
return imageResult(base64, mimeType);
|
|
123
|
+
}
|
|
124
|
+
// Non-image file - return as text (base64 encoded)
|
|
125
|
+
return textResult(response.result);
|
|
126
|
+
}
|
|
127
|
+
export async function uploadFile(client, params) {
|
|
128
|
+
// Validate exactly one source is provided
|
|
129
|
+
const sources = [params.filePath, params.url, params.base64].filter(Boolean);
|
|
130
|
+
if (sources.length === 0) {
|
|
131
|
+
throw new Error("One of filePath, url, or base64 must be provided");
|
|
132
|
+
}
|
|
133
|
+
if (sources.length > 1) {
|
|
134
|
+
throw new Error("Only one of filePath, url, or base64 should be provided");
|
|
135
|
+
}
|
|
136
|
+
let base64;
|
|
137
|
+
let mimetype;
|
|
138
|
+
let filename;
|
|
139
|
+
if (params.filePath) {
|
|
140
|
+
// Read from local file system
|
|
141
|
+
const fileData = await readLocalFile(params.filePath);
|
|
142
|
+
base64 = fileData.base64;
|
|
143
|
+
mimetype = params.mimetype || fileData.mimetype;
|
|
144
|
+
filename = params.filename || fileData.filename;
|
|
145
|
+
}
|
|
146
|
+
else if (params.url) {
|
|
147
|
+
// Fetch from remote URL
|
|
148
|
+
const fileData = await fetchRemoteFile(params.url);
|
|
149
|
+
base64 = fileData.base64;
|
|
150
|
+
mimetype = params.mimetype || fileData.mimetype;
|
|
151
|
+
filename = params.filename || fileData.filename;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Use provided base64 directly (params.base64 must be set due to validation above)
|
|
155
|
+
base64 = params.base64;
|
|
156
|
+
mimetype = params.mimetype || detectMimeTypeFromBase64(base64) || "application/octet-stream";
|
|
157
|
+
filename = params.filename;
|
|
158
|
+
}
|
|
159
|
+
const response = await client.call("file.upload", [
|
|
160
|
+
{ base64, mimetype, filename },
|
|
161
|
+
]);
|
|
162
|
+
if (!response.result) {
|
|
163
|
+
throw new Error("No URL returned from upload");
|
|
164
|
+
}
|
|
165
|
+
let url = response.result;
|
|
166
|
+
// Strip markdown image wrapper if present (API currently returns "")
|
|
167
|
+
if (url.startsWith(" && url.endsWith(")")) {
|
|
168
|
+
url = url.slice(4, -1);
|
|
169
|
+
}
|
|
170
|
+
return textResult({ url });
|
|
171
|
+
}
|
|
172
|
+
export async function deleteFile(client, params) {
|
|
173
|
+
await client.call("file.delete", [{ url: params.url }]);
|
|
174
|
+
return textResult({ deleted: true });
|
|
175
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CallToolResult } from "../types.js";
|
|
3
|
+
export declare const ListGraphsSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
4
|
+
export declare const SetupNewGraphSchema: z.ZodObject<{
|
|
5
|
+
graph: z.ZodOptional<z.ZodString>;
|
|
6
|
+
nickname: z.ZodOptional<z.ZodString>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
nickname?: string | undefined;
|
|
9
|
+
graph?: string | undefined;
|
|
10
|
+
}, {
|
|
11
|
+
nickname?: string | undefined;
|
|
12
|
+
graph?: string | undefined;
|
|
13
|
+
}>;
|
|
14
|
+
export type ListGraphsParams = z.infer<typeof ListGraphsSchema>;
|
|
15
|
+
export type SetupNewGraphParams = z.infer<typeof SetupNewGraphSchema>;
|
|
16
|
+
/**
|
|
17
|
+
* List all configured graphs with their nicknames.
|
|
18
|
+
*/
|
|
19
|
+
export declare function listGraphs(): Promise<CallToolResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Set up a new Roam graph connection, or list available graphs.
|
|
22
|
+
* Call without arguments to list available graphs from Roam Desktop.
|
|
23
|
+
* Call with graph + nickname to request a token and save the configuration.
|
|
24
|
+
*/
|
|
25
|
+
export declare function setupNewGraph(args: SetupNewGraphParams): Promise<CallToolResult>;
|
|
26
|
+
//# sourceMappingURL=graphs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphs.d.ts","sourceRoot":"","sources":["../../src/operations/graphs.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAsBlD,eAAO,MAAM,gBAAgB,gDAAe,CAAC;AAE7C,eAAO,MAAM,mBAAmB;;;;;;;;;EAoB9B,CAAC;AAMH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAChE,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAMtE;;GAEG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,cAAc,CAAC,CAmB1D;AAqDD;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,cAAc,CAAC,CAiKzB"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// src/core/operations/graphs.ts
|
|
2
|
+
// Graph management operations
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { textResult, RoamError, ErrorCodes } from "../types.js";
|
|
5
|
+
import { getConfiguredGraphs, getConfiguredGraphsSafe, getPort, saveGraphToConfig, } from "../graph-resolver.js";
|
|
6
|
+
import { fetchAvailableGraphs, requestToken, sleep, openRoamApp, slugify, } from "../roam-api.js";
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Schemas
|
|
9
|
+
// ============================================================================
|
|
10
|
+
export const ListGraphsSchema = z.object({});
|
|
11
|
+
export const SetupNewGraphSchema = z.object({
|
|
12
|
+
graph: z
|
|
13
|
+
.string()
|
|
14
|
+
.regex(/^[A-Za-z0-9_-]+$/, "Graph name must contain only letters, numbers, hyphens, and underscores")
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("The canonical Roam graph name. Omit graph and nickname to list available graphs."),
|
|
17
|
+
nickname: z
|
|
18
|
+
.string()
|
|
19
|
+
.min(1, "Nickname must not be empty")
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("A short, memorable label describing what this graph is for (e.g. 'my personal graph', 'work notes', 'book club'). " +
|
|
22
|
+
"Ask the user what they call this graph — use their natural language, not hyphenated format. " +
|
|
23
|
+
"Do not just copy the graph name. Required when graph is provided."),
|
|
24
|
+
});
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Operations
|
|
27
|
+
// ============================================================================
|
|
28
|
+
/**
|
|
29
|
+
* List all configured graphs with their nicknames.
|
|
30
|
+
*/
|
|
31
|
+
export async function listGraphs() {
|
|
32
|
+
try {
|
|
33
|
+
const graphs = await getConfiguredGraphs();
|
|
34
|
+
return textResult({
|
|
35
|
+
graphs,
|
|
36
|
+
instruction: "Pass the 'nickname' value as the graph parameter. Before operating on a graph, call get_graph_guidelines to understand its conventions.",
|
|
37
|
+
setup: "To connect additional graphs, use the setup_new_graph tool (call it without arguments to see available graphs).",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof RoamError) {
|
|
42
|
+
return textResult({
|
|
43
|
+
error: {
|
|
44
|
+
code: error.code,
|
|
45
|
+
message: error.message,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Dedup available graphs: if same name exists as both hosted and offline, keep hosted.
|
|
54
|
+
* Consistent with getMcpConfig() dedup behavior in graph-resolver.ts.
|
|
55
|
+
*/
|
|
56
|
+
function dedupAvailableGraphs(graphs) {
|
|
57
|
+
const byName = new Map();
|
|
58
|
+
for (const g of graphs) {
|
|
59
|
+
const existing = byName.get(g.name);
|
|
60
|
+
if (existing) {
|
|
61
|
+
if (g.type === "hosted")
|
|
62
|
+
byName.set(g.name, g);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
byName.set(g.name, g);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return Array.from(byName.values());
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Fetch available graphs from Roam Desktop, retrying once if Roam isn't running.
|
|
72
|
+
* Deduplicates by name (hosted takes priority over offline).
|
|
73
|
+
*/
|
|
74
|
+
async function fetchAvailableGraphsWithRetry(port) {
|
|
75
|
+
let raw;
|
|
76
|
+
try {
|
|
77
|
+
raw = await fetchAvailableGraphs(port);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const err = error;
|
|
81
|
+
const isConnectionError = err.cause?.code === "ECONNREFUSED" ||
|
|
82
|
+
err.message?.includes("fetch failed");
|
|
83
|
+
if (!isConnectionError) {
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
await openRoamApp();
|
|
87
|
+
await sleep(5000);
|
|
88
|
+
try {
|
|
89
|
+
raw = await fetchAvailableGraphs(port);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new RoamError("Could not connect to Roam Desktop. Make sure it is running and the Local API is enabled in Settings > Local API.", ErrorCodes.CONNECTION_FAILED);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return dedupAvailableGraphs(raw);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Set up a new Roam graph connection, or list available graphs.
|
|
99
|
+
* Call without arguments to list available graphs from Roam Desktop.
|
|
100
|
+
* Call with graph + nickname to request a token and save the configuration.
|
|
101
|
+
*/
|
|
102
|
+
export async function setupNewGraph(args) {
|
|
103
|
+
const { graph, nickname: rawNickname } = args;
|
|
104
|
+
// List mode: no args → return available graphs from Roam Desktop
|
|
105
|
+
if (!graph) {
|
|
106
|
+
const port = await getPort();
|
|
107
|
+
const availableGraphs = await fetchAvailableGraphsWithRetry(port);
|
|
108
|
+
const configuredGraphs = await getConfiguredGraphsSafe();
|
|
109
|
+
return textResult({
|
|
110
|
+
available_graphs: availableGraphs.map((g) => ({
|
|
111
|
+
name: g.name,
|
|
112
|
+
type: g.type,
|
|
113
|
+
})),
|
|
114
|
+
already_configured: configuredGraphs.map((g) => ({
|
|
115
|
+
name: g.name,
|
|
116
|
+
nickname: g.nickname,
|
|
117
|
+
type: g.type,
|
|
118
|
+
accessLevel: g.accessLevel,
|
|
119
|
+
lastKnownTokenStatus: g.lastKnownTokenStatus,
|
|
120
|
+
})),
|
|
121
|
+
instruction: "Call setup_new_graph with graph and nickname to connect one of the available graphs.",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// Setup mode: graph provided, nickname required
|
|
125
|
+
if (!rawNickname) {
|
|
126
|
+
throw new RoamError("nickname is required when graph is provided.", ErrorCodes.VALIDATION_ERROR);
|
|
127
|
+
}
|
|
128
|
+
// 1. Slugify nickname
|
|
129
|
+
const nickname = slugify(rawNickname);
|
|
130
|
+
if (!nickname) {
|
|
131
|
+
throw new RoamError(`Nickname "${rawNickname}" produces an empty result after converting to kebab-case. Use a nickname with at least one letter or number.`, ErrorCodes.VALIDATION_ERROR);
|
|
132
|
+
}
|
|
133
|
+
// 2. Check if graph is already configured
|
|
134
|
+
const existingGraphs = await getConfiguredGraphsSafe();
|
|
135
|
+
const matchingConfigs = existingGraphs.filter((g) => g.name === graph);
|
|
136
|
+
if (matchingConfigs.length > 0) {
|
|
137
|
+
const allRevoked = matchingConfigs.every((g) => g.lastKnownTokenStatus === "revoked");
|
|
138
|
+
if (!allRevoked) {
|
|
139
|
+
// At least one active config — return as already configured
|
|
140
|
+
return textResult({
|
|
141
|
+
status: "already_configured",
|
|
142
|
+
graphs: matchingConfigs.map((g) => ({
|
|
143
|
+
name: g.name,
|
|
144
|
+
nickname: g.nickname,
|
|
145
|
+
type: g.type,
|
|
146
|
+
accessLevel: g.accessLevel,
|
|
147
|
+
lastKnownTokenStatus: g.lastKnownTokenStatus,
|
|
148
|
+
})),
|
|
149
|
+
instruction: "This graph is already configured. Pass the 'nickname' value as the graph parameter. Call get_graph_guidelines before operating on it.",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// All revoked — fall through to re-request a new token
|
|
153
|
+
}
|
|
154
|
+
// 3. Check nickname collision
|
|
155
|
+
const nicknameCollision = existingGraphs.find((g) => g.nickname.toLowerCase() === nickname.toLowerCase() && g.name !== graph);
|
|
156
|
+
if (nicknameCollision) {
|
|
157
|
+
throw new RoamError(`Nickname "${nickname}" is already used by graph "${nicknameCollision.name}". Please choose a different nickname.`, ErrorCodes.VALIDATION_ERROR);
|
|
158
|
+
}
|
|
159
|
+
// 4. Get port and fetch available graphs
|
|
160
|
+
const port = await getPort();
|
|
161
|
+
const availableGraphs = await fetchAvailableGraphsWithRetry(port);
|
|
162
|
+
// 5. Find graph type
|
|
163
|
+
const graphInfo = availableGraphs.find((g) => g.name === graph);
|
|
164
|
+
if (!graphInfo) {
|
|
165
|
+
throw new RoamError(`Graph "${graph}" was not found in Roam Desktop. Make sure the graph name is correct and that it is available in the app.`, ErrorCodes.VALIDATION_ERROR, {
|
|
166
|
+
available_graphs: availableGraphs.map((g) => g.name),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// 6. Request token (blocks until user approves/denies in Roam)
|
|
170
|
+
const result = await requestToken(port, graph, graphInfo.type, "full");
|
|
171
|
+
// 7. Handle errors
|
|
172
|
+
if (!result.success || !result.token) {
|
|
173
|
+
const error = result.error;
|
|
174
|
+
const errorCode = error && typeof error === "object" ? error.code : undefined;
|
|
175
|
+
const errorMessage = error
|
|
176
|
+
? typeof error === "string"
|
|
177
|
+
? error
|
|
178
|
+
: error.message || "Unknown error"
|
|
179
|
+
: "Unknown error";
|
|
180
|
+
switch (errorCode) {
|
|
181
|
+
case "USER_REJECTED":
|
|
182
|
+
throw new RoamError("Token request was denied in Roam. The user must approve the request in the Roam desktop app.", ErrorCodes.USER_REJECTED);
|
|
183
|
+
case "GRAPH_BLOCKED":
|
|
184
|
+
throw new RoamError("This graph has blocked token requests. Unblock it in Roam Settings > Graph > Local API Tokens.", ErrorCodes.GRAPH_BLOCKED);
|
|
185
|
+
case "TIMEOUT":
|
|
186
|
+
throw new RoamError("No response after 5 minutes. Please try again — the user needs to approve the request in the Roam desktop app.", ErrorCodes.TIMEOUT);
|
|
187
|
+
case "REQUEST_IN_PROGRESS":
|
|
188
|
+
throw new RoamError("Another token request is already pending for this graph. The user should respond to the existing request in Roam first.", ErrorCodes.REQUEST_IN_PROGRESS);
|
|
189
|
+
default:
|
|
190
|
+
throw new RoamError(`Token request failed: ${errorMessage}`, ErrorCodes.INTERNAL_ERROR);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// 8. Save to config
|
|
194
|
+
const accessLevel = (result.grantedAccessLevel || "full");
|
|
195
|
+
const graphConfig = {
|
|
196
|
+
name: graph,
|
|
197
|
+
type: graphInfo.type,
|
|
198
|
+
token: result.token,
|
|
199
|
+
nickname,
|
|
200
|
+
accessLevel,
|
|
201
|
+
};
|
|
202
|
+
await saveGraphToConfig(graphConfig);
|
|
203
|
+
// 9. Return success
|
|
204
|
+
return textResult({
|
|
205
|
+
status: "connected",
|
|
206
|
+
graph: {
|
|
207
|
+
name: graph,
|
|
208
|
+
nickname,
|
|
209
|
+
type: graphInfo.type,
|
|
210
|
+
accessLevel,
|
|
211
|
+
},
|
|
212
|
+
instruction: "Graph connected successfully. Call get_graph_guidelines next to understand the graph's conventions before making any changes.",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { RoamClient } from "../client.js";
|
|
3
|
+
import type { CallToolResult } from "../types.js";
|
|
4
|
+
export declare const GetOpenWindowsSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
5
|
+
export declare const GetSelectionSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
6
|
+
export declare const OpenMainWindowSchema: z.ZodObject<{
|
|
7
|
+
uid: z.ZodOptional<z.ZodString>;
|
|
8
|
+
title: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
title?: string | undefined;
|
|
11
|
+
uid?: string | undefined;
|
|
12
|
+
}, {
|
|
13
|
+
title?: string | undefined;
|
|
14
|
+
uid?: string | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
export declare const OpenSidebarSchema: z.ZodObject<{
|
|
17
|
+
uid: z.ZodString;
|
|
18
|
+
type: z.ZodOptional<z.ZodEnum<["block", "outline", "mentions"]>>;
|
|
19
|
+
}, "strip", z.ZodTypeAny, {
|
|
20
|
+
uid: string;
|
|
21
|
+
type?: "mentions" | "block" | "outline" | undefined;
|
|
22
|
+
}, {
|
|
23
|
+
uid: string;
|
|
24
|
+
type?: "mentions" | "block" | "outline" | undefined;
|
|
25
|
+
}>;
|
|
26
|
+
export type OpenMainWindowParams = z.infer<typeof OpenMainWindowSchema>;
|
|
27
|
+
export type OpenSidebarParams = z.infer<typeof OpenSidebarSchema>;
|
|
28
|
+
export declare function getOpenWindows(client: RoamClient): Promise<CallToolResult>;
|
|
29
|
+
export declare function getSelection(client: RoamClient): Promise<CallToolResult>;
|
|
30
|
+
export declare function openMainWindow(client: RoamClient, params: OpenMainWindowParams): Promise<CallToolResult>;
|
|
31
|
+
export declare function openSidebar(client: RoamClient, params: OpenSidebarParams): Promise<CallToolResult>;
|
|
32
|
+
//# sourceMappingURL=navigation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/operations/navigation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAkE,cAAc,EAAE,MAAM,aAAa,CAAC;AAIlH,eAAO,MAAM,oBAAoB,gDAAe,CAAC;AAEjD,eAAO,MAAM,kBAAkB,gDAAe,CAAC;AAE/C,eAAO,MAAM,oBAAoB;;;;;;;;;EAG/B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;EAG5B,CAAC;AAGH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACxE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,wBAAsB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC,CAShF;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC,CAS9E;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAQ9G;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC,CAUxG"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { textResult } from "../types.js";
|
|
3
|
+
// Schemas
|
|
4
|
+
export const GetOpenWindowsSchema = z.object({});
|
|
5
|
+
export const GetSelectionSchema = z.object({});
|
|
6
|
+
export const OpenMainWindowSchema = z.object({
|
|
7
|
+
uid: z.string().optional().describe("UID of page or block"),
|
|
8
|
+
title: z.string().optional().describe("Page title (alternative to uid)"),
|
|
9
|
+
});
|
|
10
|
+
export const OpenSidebarSchema = z.object({
|
|
11
|
+
uid: z.string().describe("UID of page or block"),
|
|
12
|
+
type: z.enum(["block", "outline", "mentions"]).optional().describe("View type (default: outline)"),
|
|
13
|
+
});
|
|
14
|
+
export async function getOpenWindows(client) {
|
|
15
|
+
const [mainResponse, sidebarResponse] = await Promise.all([
|
|
16
|
+
client.call("ui.mainWindow.getOpenView", []),
|
|
17
|
+
client.call("ui.rightSidebar.getWindows", []),
|
|
18
|
+
]);
|
|
19
|
+
return textResult({
|
|
20
|
+
main: mainResponse.result ?? null,
|
|
21
|
+
sidebar: sidebarResponse.result ?? [],
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export async function getSelection(client) {
|
|
25
|
+
const [focusedResponse, multiSelectedResponse] = await Promise.all([
|
|
26
|
+
client.call("ui.getFocusedBlock", []),
|
|
27
|
+
client.call("ui.multiselect.getSelected", []),
|
|
28
|
+
]);
|
|
29
|
+
return textResult({
|
|
30
|
+
focused: focusedResponse.result ?? null,
|
|
31
|
+
multiSelected: multiSelectedResponse.result ?? [],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export async function openMainWindow(client, params) {
|
|
35
|
+
if (params.uid) {
|
|
36
|
+
// Could be a page or block - openBlock handles both
|
|
37
|
+
await client.call("ui.mainWindow.openBlock", [{ block: { uid: params.uid } }]);
|
|
38
|
+
}
|
|
39
|
+
else if (params.title) {
|
|
40
|
+
await client.call("ui.mainWindow.openPage", [{ page: { title: params.title } }]);
|
|
41
|
+
}
|
|
42
|
+
return textResult({ success: true });
|
|
43
|
+
}
|
|
44
|
+
export async function openSidebar(client, params) {
|
|
45
|
+
await client.call("ui.rightSidebar.addWindow", [
|
|
46
|
+
{
|
|
47
|
+
window: {
|
|
48
|
+
type: params.type || "outline",
|
|
49
|
+
"block-uid": params.uid,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
return textResult({ success: true });
|
|
54
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { RoamClient } from "../client.js";
|
|
3
|
+
import type { CallToolResult } from "../types.js";
|
|
4
|
+
export declare const CreatePageSchema: z.ZodObject<{
|
|
5
|
+
title: z.ZodString;
|
|
6
|
+
markdown: z.ZodOptional<z.ZodString>;
|
|
7
|
+
uid: z.ZodOptional<z.ZodString>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
title: string;
|
|
10
|
+
markdown?: string | undefined;
|
|
11
|
+
uid?: string | undefined;
|
|
12
|
+
}, {
|
|
13
|
+
title: string;
|
|
14
|
+
markdown?: string | undefined;
|
|
15
|
+
uid?: string | undefined;
|
|
16
|
+
}>;
|
|
17
|
+
export declare const GetPageSchema: z.ZodObject<{
|
|
18
|
+
title: z.ZodOptional<z.ZodString>;
|
|
19
|
+
uid: z.ZodOptional<z.ZodString>;
|
|
20
|
+
maxDepth: z.ZodOptional<z.ZodNumber>;
|
|
21
|
+
}, "strip", z.ZodTypeAny, {
|
|
22
|
+
title?: string | undefined;
|
|
23
|
+
uid?: string | undefined;
|
|
24
|
+
maxDepth?: number | undefined;
|
|
25
|
+
}, {
|
|
26
|
+
title?: string | undefined;
|
|
27
|
+
uid?: string | undefined;
|
|
28
|
+
maxDepth?: number | undefined;
|
|
29
|
+
}>;
|
|
30
|
+
export declare const DeletePageSchema: z.ZodObject<{
|
|
31
|
+
uid: z.ZodString;
|
|
32
|
+
}, "strip", z.ZodTypeAny, {
|
|
33
|
+
uid: string;
|
|
34
|
+
}, {
|
|
35
|
+
uid: string;
|
|
36
|
+
}>;
|
|
37
|
+
export declare const UpdatePageSchema: z.ZodObject<{
|
|
38
|
+
uid: z.ZodString;
|
|
39
|
+
title: z.ZodOptional<z.ZodString>;
|
|
40
|
+
childrenViewType: z.ZodOptional<z.ZodEnum<["document", "bullet", "numbered"]>>;
|
|
41
|
+
mergePages: z.ZodOptional<z.ZodBoolean>;
|
|
42
|
+
}, "strip", z.ZodTypeAny, {
|
|
43
|
+
uid: string;
|
|
44
|
+
title?: string | undefined;
|
|
45
|
+
childrenViewType?: "bullet" | "numbered" | "document" | undefined;
|
|
46
|
+
mergePages?: boolean | undefined;
|
|
47
|
+
}, {
|
|
48
|
+
uid: string;
|
|
49
|
+
title?: string | undefined;
|
|
50
|
+
childrenViewType?: "bullet" | "numbered" | "document" | undefined;
|
|
51
|
+
mergePages?: boolean | undefined;
|
|
52
|
+
}>;
|
|
53
|
+
export declare const GetGuidelinesSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
54
|
+
export type CreatePageParams = z.infer<typeof CreatePageSchema>;
|
|
55
|
+
export type GetPageParams = z.infer<typeof GetPageSchema>;
|
|
56
|
+
export type DeletePageParams = z.infer<typeof DeletePageSchema>;
|
|
57
|
+
export type UpdatePageParams = z.infer<typeof UpdatePageSchema>;
|
|
58
|
+
export declare function createPage(client: RoamClient, params: CreatePageParams): Promise<CallToolResult>;
|
|
59
|
+
export declare function getPage(client: RoamClient, params: GetPageParams): Promise<CallToolResult>;
|
|
60
|
+
export declare function deletePage(client: RoamClient, params: DeletePageParams): Promise<CallToolResult>;
|
|
61
|
+
export declare function updatePage(client: RoamClient, params: UpdatePageParams): Promise<CallToolResult>;
|
|
62
|
+
export declare function getGuidelines(client: RoamClient): Promise<CallToolResult>;
|
|
63
|
+
//# sourceMappingURL=pages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../src/operations/pages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAIlD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;EAI3B,CAAC;AAEH,eAAO,MAAM,aAAa;;;;;;;;;;;;EAIxB,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;EAE3B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;EAK3B,CAAC;AAEH,eAAO,MAAM,mBAAmB,gDAAe,CAAC;AAGhD,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAChE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAC1D,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAChE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE,wBAAsB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,CAAC,CAQtG;AAED,wBAAsB,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CAMhG;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,CAAC,CAGtG;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,CAAC,CAUtG;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC,CAK/E"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { textResult } from "../types.js";
|
|
3
|
+
// Schemas
|
|
4
|
+
export const CreatePageSchema = z.object({
|
|
5
|
+
title: z.string().describe("Page title"),
|
|
6
|
+
markdown: z.string().optional().describe("Markdown content for the page"),
|
|
7
|
+
uid: z.string().optional(),
|
|
8
|
+
});
|
|
9
|
+
export const GetPageSchema = z.object({
|
|
10
|
+
title: z.string().optional().describe("Page title (alternative to uid)"),
|
|
11
|
+
uid: z.string().optional().describe("Page UID"),
|
|
12
|
+
maxDepth: z.coerce.number().optional().describe("Max depth of children to include in markdown (omit for full tree)"),
|
|
13
|
+
});
|
|
14
|
+
export const DeletePageSchema = z.object({
|
|
15
|
+
uid: z.string().describe("Page UID to delete"),
|
|
16
|
+
});
|
|
17
|
+
export const UpdatePageSchema = z.object({
|
|
18
|
+
uid: z.string().describe("Page UID"),
|
|
19
|
+
title: z.string().optional().describe("New page title"),
|
|
20
|
+
childrenViewType: z.enum(["document", "bullet", "numbered"]).optional().describe("How children are displayed (document, bullet, or numbered)"),
|
|
21
|
+
mergePages: z.boolean().optional().describe("If true, merge with existing page when renaming to a title that already exists (default: false)"),
|
|
22
|
+
});
|
|
23
|
+
export const GetGuidelinesSchema = z.object({});
|
|
24
|
+
export async function createPage(client, params) {
|
|
25
|
+
const response = await client.call("data.page.fromMarkdown", [
|
|
26
|
+
{
|
|
27
|
+
page: { title: params.title, uid: params.uid },
|
|
28
|
+
"markdown-string": params.markdown,
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
return textResult(response.result ?? { uid: "" });
|
|
32
|
+
}
|
|
33
|
+
export async function getPage(client, params) {
|
|
34
|
+
const apiParams = params.uid ? { uid: params.uid } : { title: params.title };
|
|
35
|
+
if (params.maxDepth !== undefined)
|
|
36
|
+
apiParams.maxDepth = params.maxDepth;
|
|
37
|
+
const response = await client.call("data.ai.getPage", [apiParams]);
|
|
38
|
+
return textResult(response.result ?? null);
|
|
39
|
+
}
|
|
40
|
+
export async function deletePage(client, params) {
|
|
41
|
+
await client.call("data.page.delete", [{ page: { uid: params.uid } }]);
|
|
42
|
+
return textResult({ success: true });
|
|
43
|
+
}
|
|
44
|
+
export async function updatePage(client, params) {
|
|
45
|
+
const page = { uid: params.uid };
|
|
46
|
+
if (params.title !== undefined)
|
|
47
|
+
page.title = params.title;
|
|
48
|
+
if (params.childrenViewType !== undefined)
|
|
49
|
+
page["children-view-type"] = params.childrenViewType;
|
|
50
|
+
const apiParams = { page };
|
|
51
|
+
if (params.mergePages !== undefined)
|
|
52
|
+
apiParams["merge-pages"] = params.mergePages;
|
|
53
|
+
await client.call("data.page.update", [apiParams]);
|
|
54
|
+
return textResult({ success: true });
|
|
55
|
+
}
|
|
56
|
+
export async function getGuidelines(client) {
|
|
57
|
+
const response = await client.call("data.ai.getGraphGuidelines", []);
|
|
58
|
+
return textResult(response.result ?? { guidelines: null, starredPages: [] });
|
|
59
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { RoamClient } from "../client.js";
|
|
3
|
+
import type { CallToolResult } from "../types.js";
|
|
4
|
+
export declare const QuerySchema: z.ZodObject<{
|
|
5
|
+
uid: z.ZodOptional<z.ZodString>;
|
|
6
|
+
query: z.ZodOptional<z.ZodString>;
|
|
7
|
+
sort: z.ZodOptional<z.ZodEnum<["created-date", "edited-date", "daily-note-date"]>>;
|
|
8
|
+
sortOrder: z.ZodOptional<z.ZodEnum<["asc", "desc"]>>;
|
|
9
|
+
includePath: z.ZodOptional<z.ZodBoolean>;
|
|
10
|
+
offset: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
12
|
+
maxDepth: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
sort?: "created-date" | "edited-date" | "daily-note-date" | undefined;
|
|
15
|
+
uid?: string | undefined;
|
|
16
|
+
maxDepth?: number | undefined;
|
|
17
|
+
offset?: number | undefined;
|
|
18
|
+
limit?: number | undefined;
|
|
19
|
+
sortOrder?: "asc" | "desc" | undefined;
|
|
20
|
+
includePath?: boolean | undefined;
|
|
21
|
+
query?: string | undefined;
|
|
22
|
+
}, {
|
|
23
|
+
sort?: "created-date" | "edited-date" | "daily-note-date" | undefined;
|
|
24
|
+
uid?: string | undefined;
|
|
25
|
+
maxDepth?: number | undefined;
|
|
26
|
+
offset?: number | undefined;
|
|
27
|
+
limit?: number | undefined;
|
|
28
|
+
sortOrder?: "asc" | "desc" | undefined;
|
|
29
|
+
includePath?: boolean | undefined;
|
|
30
|
+
query?: string | undefined;
|
|
31
|
+
}>;
|
|
32
|
+
export type QueryParams = z.infer<typeof QuerySchema>;
|
|
33
|
+
export declare function query(client: RoamClient, params: QueryParams): Promise<CallToolResult>;
|
|
34
|
+
//# sourceMappingURL=query.d.ts.map
|