@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
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @roam-research/roam-tools-core
|
|
2
|
+
|
|
3
|
+
Shared core library for Roam Research tools. Provides the Roam API client, tool definitions, graph resolution, and operations.
|
|
4
|
+
|
|
5
|
+
> **This package is not meant to be used directly.** It is an internal dependency of [`@roam-research/roam-mcp`](https://www.npmjs.com/package/@roam-research/roam-mcp) (MCP server) and [`@roam-research/roam-cli`](https://www.npmjs.com/package/@roam-research/roam-cli) (CLI). If you want to connect an AI assistant to Roam, install `@roam-research/roam-mcp`. If you want a command-line interface, install `@roam-research/roam-cli`.
|
|
6
|
+
|
|
7
|
+
## What's in here
|
|
8
|
+
|
|
9
|
+
- **RoamClient** — authenticated HTTP client for Roam's local API
|
|
10
|
+
- **Tool definitions** — Zod-validated tool schemas used by both MCP and CLI
|
|
11
|
+
- **Operations** — page, block, search, query, file, and navigation operations
|
|
12
|
+
- **Graph resolution** — config loading, graph lookup, and multi-graph support
|
|
13
|
+
- **Types** — shared TypeScript types, error codes, and schemas
|
|
14
|
+
|
|
15
|
+
## Documentation
|
|
16
|
+
|
|
17
|
+
See the [main repository](https://github.com/Roam-Research/roam-tools) for full documentation.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RoamResponse, RoamClientConfig, TokenInfoResult } from "./types.js";
|
|
2
|
+
export declare class RoamClient {
|
|
3
|
+
private graphName;
|
|
4
|
+
private graphType;
|
|
5
|
+
private token;
|
|
6
|
+
private port;
|
|
7
|
+
constructor(config: RoamClientConfig);
|
|
8
|
+
private getPort;
|
|
9
|
+
private openRoamDeepLink;
|
|
10
|
+
private sleep;
|
|
11
|
+
private isConnectionError;
|
|
12
|
+
private isVersionMismatch;
|
|
13
|
+
private handleVersionMismatch;
|
|
14
|
+
/**
|
|
15
|
+
* Get user-friendly guidance for authentication errors
|
|
16
|
+
*/
|
|
17
|
+
private getAuthErrorGuidance;
|
|
18
|
+
/**
|
|
19
|
+
* Get user-friendly guidance for permission errors
|
|
20
|
+
*/
|
|
21
|
+
private getPermissionErrorGuidance;
|
|
22
|
+
/**
|
|
23
|
+
* Handle API error responses based on HTTP status and error code
|
|
24
|
+
*/
|
|
25
|
+
private handleApiError;
|
|
26
|
+
private checkResponse;
|
|
27
|
+
/**
|
|
28
|
+
* Query the token info endpoint for current permissions.
|
|
29
|
+
* Best-effort: returns "unknown" on any failure except confirmed revocation.
|
|
30
|
+
*/
|
|
31
|
+
getTokenInfo(): Promise<TokenInfoResult>;
|
|
32
|
+
call<T = unknown>(action: string, args?: unknown[]): Promise<RoamResponse<T>>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAGhB,eAAe,EAEhB,MAAM,YAAY,CAAC;AAQpB,qBAAa,UAAU;IACrB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,IAAI,CAAuB;gBAEvB,MAAM,EAAE,gBAAgB;YAkBtB,OAAO;YAeP,gBAAgB;YAKhB,KAAK;IAInB,OAAO,CAAC,iBAAiB;IAuBzB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,qBAAqB;IAyB7B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAgC5B;;OAEG;IACH,OAAO,CAAC,0BAA0B;IAoBlC;;OAEG;IACH,OAAO,CAAC,cAAc;IAgDtB,OAAO,CAAC,aAAa;IASrB;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,eAAe,CAAC;IA0CxC,IAAI,CAAC,CAAC,GAAG,OAAO,EACpB,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,OAAO,EAAO,GACnB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;CAuE5B"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// src/core/client.ts
|
|
2
|
+
// v2.0.0 - Token-authenticated Roam Local API client
|
|
3
|
+
import { readFile } from "fs/promises";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import open from "open";
|
|
7
|
+
import { EXPECTED_API_VERSION, getErrorMessage, RoamError, ErrorCodes, } from "./types.js";
|
|
8
|
+
export class RoamClient {
|
|
9
|
+
graphName;
|
|
10
|
+
graphType;
|
|
11
|
+
token;
|
|
12
|
+
port = null;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
if (!config.graphName) {
|
|
15
|
+
throw new Error("graphName is required");
|
|
16
|
+
}
|
|
17
|
+
if (!/^[A-Za-z0-9_-]+$/.test(config.graphName)) {
|
|
18
|
+
throw new Error(`Invalid graph name "${config.graphName}". Graph names can only contain letters, numbers, hyphens, and underscores.`);
|
|
19
|
+
}
|
|
20
|
+
if (!config.token) {
|
|
21
|
+
throw new Error("token is required");
|
|
22
|
+
}
|
|
23
|
+
this.graphName = config.graphName;
|
|
24
|
+
this.graphType = config.graphType;
|
|
25
|
+
this.token = config.token;
|
|
26
|
+
if (config.port) {
|
|
27
|
+
this.port = config.port;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async getPort() {
|
|
31
|
+
if (this.port)
|
|
32
|
+
return this.port;
|
|
33
|
+
try {
|
|
34
|
+
const configFile = join(homedir(), ".roam-local-api.json");
|
|
35
|
+
const content = await readFile(configFile, "utf-8");
|
|
36
|
+
const config = JSON.parse(content);
|
|
37
|
+
this.port = config.port;
|
|
38
|
+
return this.port;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Default port if file doesn't exist
|
|
42
|
+
return 3333;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async openRoamDeepLink() {
|
|
46
|
+
const deepLink = `roam://#/app/${this.graphName}`;
|
|
47
|
+
await open(deepLink);
|
|
48
|
+
}
|
|
49
|
+
async sleep(ms) {
|
|
50
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
51
|
+
}
|
|
52
|
+
isConnectionError(error) {
|
|
53
|
+
if (error instanceof Error) {
|
|
54
|
+
if (error.message.includes("ECONNREFUSED") ||
|
|
55
|
+
error.message.includes("fetch failed") ||
|
|
56
|
+
error.message.includes("network")) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
// Check error.cause for Node fetch network errors
|
|
60
|
+
if (error.cause && error.cause instanceof Error) {
|
|
61
|
+
const causeCode = error.cause.code;
|
|
62
|
+
return (causeCode === "ECONNREFUSED" ||
|
|
63
|
+
causeCode === "ECONNRESET" ||
|
|
64
|
+
causeCode === "ENOTFOUND" ||
|
|
65
|
+
causeCode === "ETIMEDOUT");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
isVersionMismatch(response) {
|
|
71
|
+
if (response.success)
|
|
72
|
+
return false;
|
|
73
|
+
const error = response.error;
|
|
74
|
+
if (typeof error === "object" && error !== null) {
|
|
75
|
+
return error.code === "VERSION_MISMATCH";
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
handleVersionMismatch(response) {
|
|
80
|
+
const serverVersion = response.apiVersion ?? "unknown";
|
|
81
|
+
let advice = "Please update Roam or the MCP server so versions match.";
|
|
82
|
+
if (serverVersion !== "unknown") {
|
|
83
|
+
const [serverMajor, serverMinor] = serverVersion.split(".").map(Number);
|
|
84
|
+
const [expectedMajor, expectedMinor] = EXPECTED_API_VERSION.split(".").map(Number);
|
|
85
|
+
if (serverMajor > expectedMajor ||
|
|
86
|
+
(serverMajor === expectedMajor && serverMinor > expectedMinor)) {
|
|
87
|
+
advice = "Please update the MCP server.";
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
advice = "Please update Roam.";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw new RoamError(`Roam API version mismatch! Roam API: ${serverVersion}, MCP expected: ${EXPECTED_API_VERSION}. ${advice}`, ErrorCodes.VERSION_MISMATCH);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get user-friendly guidance for authentication errors
|
|
97
|
+
*/
|
|
98
|
+
getAuthErrorGuidance(code) {
|
|
99
|
+
const baseMsg = "Authentication failed. ";
|
|
100
|
+
switch (code) {
|
|
101
|
+
case ErrorCodes.MISSING_TOKEN:
|
|
102
|
+
return (baseMsg +
|
|
103
|
+
"No API token provided. Please ensure your ~/.roam-tools.json has a valid token.");
|
|
104
|
+
case ErrorCodes.INVALID_TOKEN_FORMAT:
|
|
105
|
+
return (baseMsg +
|
|
106
|
+
"The token format is invalid. Tokens should start with 'roam-graph-local-token-'.");
|
|
107
|
+
case ErrorCodes.WRONG_GRAPH_TYPE:
|
|
108
|
+
return (baseMsg +
|
|
109
|
+
"This token is for a different graph type. Check that 'type' matches in your config.");
|
|
110
|
+
case ErrorCodes.TOKEN_NOT_FOUND:
|
|
111
|
+
return (baseMsg +
|
|
112
|
+
"The token was not recognized. It may have been revoked. Create a new token in Roam Settings > Graph > Local API Tokens.");
|
|
113
|
+
default:
|
|
114
|
+
return (baseMsg +
|
|
115
|
+
"Please check your token in ~/.roam-tools.json. Create a token in Roam Settings > Graph > Local API Tokens.");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get user-friendly guidance for permission errors
|
|
120
|
+
*/
|
|
121
|
+
getPermissionErrorGuidance(code, error) {
|
|
122
|
+
switch (code) {
|
|
123
|
+
case ErrorCodes.INSUFFICIENT_SCOPE:
|
|
124
|
+
return (`Permission denied. ${error?.message || "This operation requires higher permissions."}\n` +
|
|
125
|
+
"Create a token with the required scope in Roam Settings > Graph > Local API Tokens.");
|
|
126
|
+
case ErrorCodes.SCOPE_EXCEEDS_PERMISSION:
|
|
127
|
+
return ("The token has more permissions than your user account allows. " +
|
|
128
|
+
"Please check your Roam user permissions for this graph.");
|
|
129
|
+
default:
|
|
130
|
+
return error?.message || "Access denied. Please check your permissions.";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Handle API error responses based on HTTP status and error code
|
|
135
|
+
*/
|
|
136
|
+
handleApiError(status, response) {
|
|
137
|
+
const error = typeof response.error === "object" ? response.error : undefined;
|
|
138
|
+
const code = error?.code;
|
|
139
|
+
const message = getErrorMessage(response.error);
|
|
140
|
+
// Version mismatch - fatal
|
|
141
|
+
if (this.isVersionMismatch(response)) {
|
|
142
|
+
this.handleVersionMismatch(response);
|
|
143
|
+
}
|
|
144
|
+
// 401 - Authentication errors
|
|
145
|
+
if (status === 401) {
|
|
146
|
+
throw new RoamError(this.getAuthErrorGuidance(code), code);
|
|
147
|
+
}
|
|
148
|
+
// 403 - Permission errors
|
|
149
|
+
if (status === 403) {
|
|
150
|
+
throw new RoamError(this.getPermissionErrorGuidance(code, error), code);
|
|
151
|
+
}
|
|
152
|
+
// 404 - Unknown action
|
|
153
|
+
if (status === 404) {
|
|
154
|
+
throw new RoamError(`Unknown API action: ${message}`, ErrorCodes.UNKNOWN_ACTION);
|
|
155
|
+
}
|
|
156
|
+
// 500 - Server errors
|
|
157
|
+
if (status >= 500) {
|
|
158
|
+
const hint = message.toLowerCase().includes("promise error")
|
|
159
|
+
? "\n\nThis can happen if the graph was closed before the request completed, " +
|
|
160
|
+
"especially for encrypted graphs, when closed before the password was entered."
|
|
161
|
+
: "";
|
|
162
|
+
throw new RoamError(`Server error: ${message}${hint}`, ErrorCodes.INTERNAL_ERROR);
|
|
163
|
+
}
|
|
164
|
+
// Other errors
|
|
165
|
+
throw new RoamError(message, code);
|
|
166
|
+
}
|
|
167
|
+
checkResponse(response, httpStatus) {
|
|
168
|
+
if (!response.success) {
|
|
169
|
+
this.handleApiError(httpStatus, response);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Query the token info endpoint for current permissions.
|
|
174
|
+
* Best-effort: returns "unknown" on any failure except confirmed revocation.
|
|
175
|
+
*/
|
|
176
|
+
async getTokenInfo() {
|
|
177
|
+
try {
|
|
178
|
+
const port = await this.getPort();
|
|
179
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/graphs/tokens/info`, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: { "Content-Type": "application/json" },
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
token: this.token,
|
|
184
|
+
graph: this.graphName,
|
|
185
|
+
type: this.graphType,
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
if (response.status === 401) {
|
|
189
|
+
try {
|
|
190
|
+
const data = (await response.json());
|
|
191
|
+
const code = typeof data.error === "object" ? data.error?.code : undefined;
|
|
192
|
+
if (code === "TOKEN_NOT_FOUND") {
|
|
193
|
+
return { status: "revoked" };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Couldn't parse 401 body
|
|
198
|
+
}
|
|
199
|
+
return { status: "unknown" };
|
|
200
|
+
}
|
|
201
|
+
if (!response.ok)
|
|
202
|
+
return { status: "unknown" };
|
|
203
|
+
const data = (await response.json());
|
|
204
|
+
if (!data.success)
|
|
205
|
+
return { status: "unknown" };
|
|
206
|
+
return { status: "active", info: data };
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return { status: "unknown" };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async call(action, args = []) {
|
|
213
|
+
const doRequest = async () => {
|
|
214
|
+
const port = await this.getPort();
|
|
215
|
+
// Build URL with graph name and optional type parameter
|
|
216
|
+
let url = `http://127.0.0.1:${port}/api/${this.graphName}`;
|
|
217
|
+
if (this.graphType === "offline") {
|
|
218
|
+
url += "?type=offline";
|
|
219
|
+
}
|
|
220
|
+
const response = await fetch(url, {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: {
|
|
223
|
+
"Content-Type": "application/json",
|
|
224
|
+
Authorization: `Bearer ${this.token}`,
|
|
225
|
+
},
|
|
226
|
+
body: JSON.stringify({
|
|
227
|
+
action,
|
|
228
|
+
args,
|
|
229
|
+
expectedApiVersion: EXPECTED_API_VERSION,
|
|
230
|
+
}),
|
|
231
|
+
});
|
|
232
|
+
const data = (await response.json());
|
|
233
|
+
return { data, status: response.status };
|
|
234
|
+
};
|
|
235
|
+
try {
|
|
236
|
+
const { data, status } = await doRequest();
|
|
237
|
+
this.checkResponse(data, status);
|
|
238
|
+
return data;
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
// If connection failed, try opening Roam and retry
|
|
242
|
+
if (this.isConnectionError(error)) {
|
|
243
|
+
// Reset cached port so we re-read from config after Roam starts
|
|
244
|
+
this.port = null;
|
|
245
|
+
try {
|
|
246
|
+
await this.openRoamDeepLink();
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// Best-effort — don't let deep link failure prevent retries
|
|
250
|
+
}
|
|
251
|
+
let delay = 500;
|
|
252
|
+
const maxDelay = 15000;
|
|
253
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
254
|
+
await this.sleep(delay);
|
|
255
|
+
try {
|
|
256
|
+
const { data, status } = await doRequest();
|
|
257
|
+
this.checkResponse(data, status);
|
|
258
|
+
return data;
|
|
259
|
+
}
|
|
260
|
+
catch (retryError) {
|
|
261
|
+
if (!this.isConnectionError(retryError)) {
|
|
262
|
+
throw retryError;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
delay = Math.min(delay * 2, maxDelay);
|
|
266
|
+
}
|
|
267
|
+
// All retries exhausted
|
|
268
|
+
throw new RoamError("Could not connect to Roam Desktop after multiple attempts. " +
|
|
269
|
+
"Please restart the Roam desktop app and also this app and then retry again. " +
|
|
270
|
+
"If you continue having issues, please let us know at support@roamresearch.com.", ErrorCodes.CONNECTION_FAILED);
|
|
271
|
+
}
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ConnectOptions {
|
|
2
|
+
graph?: string;
|
|
3
|
+
nickname?: string;
|
|
4
|
+
accessLevel?: string;
|
|
5
|
+
public?: boolean;
|
|
6
|
+
type?: string;
|
|
7
|
+
remove?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function connect(options?: ConnectOptions): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=connect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AA8BA,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AA0CD,wBAAsB,OAAO,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgiBzE"}
|