@roam-research/roam-tools-core 0.5.1 → 0.6.1
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 +23 -8
- package/dist/index.d.ts +3 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/operations/blocks.d.ts +21 -22
- package/dist/operations/blocks.d.ts.map +1 -1
- package/dist/operations/datalog.d.ts +2 -3
- package/dist/operations/datalog.d.ts.map +1 -1
- package/dist/operations/files.d.ts +6 -7
- package/dist/operations/files.d.ts.map +1 -1
- package/dist/operations/navigation.d.ts +9 -10
- package/dist/operations/navigation.d.ts.map +1 -1
- package/dist/operations/pages.d.ts +10 -11
- package/dist/operations/pages.d.ts.map +1 -1
- package/dist/operations/query.d.ts +10 -11
- package/dist/operations/query.d.ts.map +1 -1
- package/dist/operations/search.d.ts +7 -8
- package/dist/operations/search.d.ts.map +1 -1
- package/dist/tools.d.ts +37 -4
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +47 -52
- package/dist/types.d.ts +17 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -0
- package/package.json +7 -10
- package/dist/client.d.ts +0 -34
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -275
- package/dist/connect.d.ts +0 -10
- package/dist/connect.d.ts.map +0 -1
- package/dist/connect.js +0 -475
- package/dist/graph-resolver.d.ts +0 -54
- package/dist/graph-resolver.d.ts.map +0 -1
- package/dist/graph-resolver.js +0 -339
- package/dist/operations/graphs.d.ts +0 -26
- package/dist/operations/graphs.d.ts.map +0 -1
- package/dist/operations/graphs.js +0 -213
- package/dist/roam-api.d.ts +0 -32
- package/dist/roam-api.d.ts.map +0 -1
- package/dist/roam-api.js +0 -50
package/dist/client.js
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/connect.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
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
|
package/dist/connect.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AA0BA,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,CA4gBzE"}
|