@roam-research/roam-tools-core 0.5.0 → 0.6.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 (47) hide show
  1. package/README.md +23 -8
  2. package/dist/index.d.ts +3 -7
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +8 -8
  5. package/dist/operations/blocks.d.ts +25 -20
  6. package/dist/operations/blocks.d.ts.map +1 -1
  7. package/dist/operations/blocks.js +66 -15
  8. package/dist/operations/datalog.d.ts +15 -0
  9. package/dist/operations/datalog.d.ts.map +1 -0
  10. package/dist/operations/datalog.js +16 -0
  11. package/dist/operations/files.d.ts +6 -7
  12. package/dist/operations/files.d.ts.map +1 -1
  13. package/dist/operations/files.js +18 -5
  14. package/dist/operations/navigation.d.ts +9 -10
  15. package/dist/operations/navigation.d.ts.map +1 -1
  16. package/dist/operations/navigation.js +4 -1
  17. package/dist/operations/pages.d.ts +15 -13
  18. package/dist/operations/pages.d.ts.map +1 -1
  19. package/dist/operations/pages.js +34 -10
  20. package/dist/operations/query.d.ts +10 -11
  21. package/dist/operations/query.d.ts.map +1 -1
  22. package/dist/operations/query.js +24 -6
  23. package/dist/operations/search.d.ts +7 -8
  24. package/dist/operations/search.d.ts.map +1 -1
  25. package/dist/operations/search.js +22 -6
  26. package/dist/tools.d.ts +37 -4
  27. package/dist/tools.d.ts.map +1 -1
  28. package/dist/tools.js +91 -80
  29. package/dist/types.d.ts +11 -10
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/types.js +14 -4
  32. package/package.json +7 -10
  33. package/dist/client.d.ts +0 -34
  34. package/dist/client.d.ts.map +0 -1
  35. package/dist/client.js +0 -275
  36. package/dist/connect.d.ts +0 -10
  37. package/dist/connect.d.ts.map +0 -1
  38. package/dist/connect.js +0 -477
  39. package/dist/graph-resolver.d.ts +0 -54
  40. package/dist/graph-resolver.d.ts.map +0 -1
  41. package/dist/graph-resolver.js +0 -338
  42. package/dist/operations/graphs.d.ts +0 -26
  43. package/dist/operations/graphs.d.ts.map +0 -1
  44. package/dist/operations/graphs.js +0 -214
  45. package/dist/roam-api.d.ts +0 -32
  46. package/dist/roam-api.d.ts.map +0 -1
  47. package/dist/roam-api.js +0 -50
@@ -1,338 +0,0 @@
1
- // src/core/graph-resolver.ts
2
- // Stateless config-based graph resolution with token authentication
3
- import { readFile, writeFile, chmod, stat } from "fs/promises";
4
- import { homedir } from "os";
5
- import { join } from "path";
6
- import { RoamMcpConfigSchema, RoamError, ErrorCodes, CONFIG_VERSION, } from "./types.js";
7
- // Warning suppression flags (prevent spamming on every tool call)
8
- let permissionCheckDone = false;
9
- let dedupWarningShown = false;
10
- async function getLocalApiConfig() {
11
- try {
12
- const configFile = join(homedir(), ".roam-local-api.json");
13
- const content = await readFile(configFile, "utf-8");
14
- return JSON.parse(content);
15
- }
16
- catch {
17
- return { port: 3333 }; // Default port
18
- }
19
- }
20
- export async function getPort() {
21
- const config = await getLocalApiConfig();
22
- return config.port;
23
- }
24
- // ============================================================================
25
- // MCP Config Loading (from ~/.roam-tools.json)
26
- // ============================================================================
27
- const CONFIG_PATH = join(homedir(), ".roam-tools.json");
28
- /**
29
- * Write config file with restricted permissions (owner read/write only).
30
- */
31
- async function writeConfigFile(path, data) {
32
- await writeFile(path, data, { mode: 0o600 });
33
- // Also chmod in case the file already existed with wrong permissions
34
- await chmod(path, 0o600);
35
- }
36
- /**
37
- * Check that the config file version is not newer than what this client supports.
38
- * Must run on raw parsed JSON BEFORE Zod validation — a future version may change
39
- * the schema structure, and Zod would reject it with a confusing error.
40
- */
41
- function checkConfigVersion(parsed) {
42
- const version = typeof parsed.version === "number" ? parsed.version : 1;
43
- if (version > CONFIG_VERSION) {
44
- throw new RoamError(`Your ~/.roam-tools.json (version ${version}) was written by a newer version of Roam tools. ` +
45
- `This client only supports config version ${CONFIG_VERSION}. ` +
46
- `Please update @roam-research/roam-mcp and @roam-research/roam-cli to the latest version.`, ErrorCodes.CONFIG_TOO_NEW, { configVersion: version, supportedVersion: CONFIG_VERSION });
47
- }
48
- }
49
- export async function getMcpConfig() {
50
- let content;
51
- try {
52
- content = await readFile(CONFIG_PATH, "utf-8");
53
- // Check file permissions (Unix only — skip on Windows)
54
- if (!permissionCheckDone) {
55
- permissionCheckDone = true;
56
- try {
57
- const fileStat = await stat(CONFIG_PATH);
58
- const mode = fileStat.mode & 0o777;
59
- if (mode & 0o077) {
60
- console.error(`[roam-mcp] WARNING: ${CONFIG_PATH} has overly permissive permissions (0${mode.toString(8)}). ` +
61
- `This file contains API tokens and should not be accessible by others. ` +
62
- `Run: chmod 600 ${CONFIG_PATH}`);
63
- }
64
- }
65
- catch {
66
- // Ignore permission check errors (e.g., Windows)
67
- }
68
- }
69
- }
70
- catch (error) {
71
- if (error.code === "ENOENT") {
72
- throw new RoamError(`No graphs configured. Use the setup_new_graph tool to connect a Roam graph, ` +
73
- `or run the CLI setup command:\n\n` +
74
- ` npx @roam-research/roam-cli connect\n\n` +
75
- `After connecting, try your request again.`, ErrorCodes.CONFIG_NOT_FOUND);
76
- }
77
- throw error;
78
- }
79
- let parsed;
80
- try {
81
- parsed = JSON.parse(content);
82
- }
83
- catch {
84
- throw new RoamError(`Invalid JSON in ${CONFIG_PATH}. Please check the file format.`, ErrorCodes.VALIDATION_ERROR);
85
- }
86
- checkConfigVersion(parsed);
87
- const validated = RoamMcpConfigSchema.safeParse(parsed);
88
- if (!validated.success) {
89
- const issues = validated.error.issues
90
- .map((i) => ` - ${i.path.join(".")}: ${i.message}`)
91
- .join("\n");
92
- throw new RoamError(`Invalid config in ${CONFIG_PATH}:\n${issues}`, ErrorCodes.VALIDATION_ERROR);
93
- }
94
- // Validate nickname uniqueness (case-insensitive)
95
- const nicknames = new Set();
96
- for (const graph of validated.data.graphs) {
97
- const lowerNickname = graph.nickname.toLowerCase();
98
- if (nicknames.has(lowerNickname)) {
99
- throw new RoamError(`Duplicate nickname "${graph.nickname}" in config. Nicknames must be unique (case-insensitive).`, ErrorCodes.VALIDATION_ERROR);
100
- }
101
- nicknames.add(lowerNickname);
102
- }
103
- // Handle same-name collisions: if both hosted and offline exist with same name, keep hosted
104
- const graphsByName = new Map();
105
- for (const graph of validated.data.graphs) {
106
- const existing = graphsByName.get(graph.name);
107
- if (existing) {
108
- if (!dedupWarningShown) {
109
- dedupWarningShown = true;
110
- // If existing is hosted, skip the new one (regardless of type)
111
- // If existing is offline and new is hosted, replace with hosted
112
- if (existing.type === "hosted") {
113
- console.error(`[roam-mcp] Warning: Ignoring duplicate graph "${graph.name}" (${graph.type}), using hosted version`);
114
- }
115
- else if (graph.type === "hosted") {
116
- console.error(`[roam-mcp] Warning: Replacing offline graph "${graph.name}" with hosted version`);
117
- }
118
- else {
119
- console.error(`[roam-mcp] Warning: Ignoring duplicate offline graph "${graph.name}"`);
120
- }
121
- }
122
- // Apply dedup logic regardless of whether warning was shown
123
- if (existing.type === "hosted") {
124
- continue;
125
- }
126
- else if (graph.type === "hosted") {
127
- graphsByName.set(graph.name, graph);
128
- }
129
- // else: both offline, skip the duplicate (continue already handled hosted case)
130
- }
131
- else {
132
- graphsByName.set(graph.name, graph);
133
- }
134
- }
135
- // Rebuild graphs array with deduplication applied
136
- return { version: validated.data.version ?? 1, graphs: Array.from(graphsByName.values()) };
137
- }
138
- // ============================================================================
139
- // Config Writing Functions
140
- // ============================================================================
141
- /**
142
- * Read the raw config file for write operations (no dedup, but validates with Zod).
143
- * Returns empty config if file doesn't exist (so saveGraphToConfig can create the initial file).
144
- */
145
- async function readRawConfig() {
146
- let content;
147
- try {
148
- content = await readFile(CONFIG_PATH, "utf-8");
149
- }
150
- catch (error) {
151
- if (error.code === "ENOENT") {
152
- return { version: CONFIG_VERSION, graphs: [] };
153
- }
154
- throw error;
155
- }
156
- let parsed;
157
- try {
158
- parsed = JSON.parse(content);
159
- }
160
- catch {
161
- throw new RoamError(`Invalid JSON in ${CONFIG_PATH}. Please check the file format.`, ErrorCodes.VALIDATION_ERROR);
162
- }
163
- checkConfigVersion(parsed);
164
- const validated = RoamMcpConfigSchema.safeParse(parsed);
165
- if (!validated.success) {
166
- const issues = validated.error.issues
167
- .map((i) => ` - ${i.path.join(".")}: ${i.message}`)
168
- .join("\n");
169
- throw new RoamError(`Invalid config in ${CONFIG_PATH}:\n${issues}`, ErrorCodes.VALIDATION_ERROR);
170
- }
171
- return validated.data;
172
- }
173
- /**
174
- * Save a graph configuration to ~/.roam-tools.json
175
- * If a graph with the same name+type exists, it will be updated.
176
- * Otherwise, the graph will be added.
177
- */
178
- export async function saveGraphToConfig(newGraph) {
179
- const config = await readRawConfig();
180
- // Check for nickname collision (case-insensitive)
181
- const existingNickname = config.graphs.find((g) => g.nickname.toLowerCase() === newGraph.nickname.toLowerCase() &&
182
- !(g.name === newGraph.name && g.type === newGraph.type));
183
- if (existingNickname) {
184
- throw new RoamError(`Nickname "${newGraph.nickname}" is already used by graph "${existingNickname.name}". Please choose a different nickname.`, ErrorCodes.VALIDATION_ERROR);
185
- }
186
- // Check for existing graph with same name+type
187
- const existingIndex = config.graphs.findIndex((g) => g.name === newGraph.name && g.type === newGraph.type);
188
- if (existingIndex >= 0) {
189
- config.graphs[existingIndex] = newGraph; // Update existing
190
- }
191
- else {
192
- config.graphs.push(newGraph); // Add new
193
- }
194
- const output = { version: CONFIG_VERSION, graphs: config.graphs };
195
- await writeConfigFile(CONFIG_PATH, JSON.stringify(output, null, 2));
196
- }
197
- /**
198
- * Remove a graph from ~/.roam-tools.json by nickname
199
- */
200
- export async function removeGraphFromConfig(nickname) {
201
- const config = await readRawConfig();
202
- const lowerNickname = nickname.toLowerCase();
203
- const initialLength = config.graphs.length;
204
- config.graphs = config.graphs.filter((g) => g.nickname.toLowerCase() !== lowerNickname);
205
- if (config.graphs.length === initialLength) {
206
- return false; // Graph not found
207
- }
208
- const output = { version: CONFIG_VERSION, graphs: config.graphs };
209
- await writeConfigFile(CONFIG_PATH, JSON.stringify(output, null, 2));
210
- return true;
211
- }
212
- /**
213
- * Update a graph's access level and/or token status in config.
214
- * No-ops if nothing changed (avoids unnecessary disk writes).
215
- */
216
- export async function updateGraphTokenStatus(nickname, updates) {
217
- const config = await readRawConfig();
218
- const graph = config.graphs.find((g) => g.nickname.toLowerCase() === nickname.toLowerCase());
219
- if (!graph)
220
- return;
221
- let changed = false;
222
- if (updates.accessLevel !== undefined && graph.accessLevel !== updates.accessLevel) {
223
- graph.accessLevel = updates.accessLevel;
224
- changed = true;
225
- }
226
- if (updates.lastKnownTokenStatus !== undefined && graph.lastKnownTokenStatus !== updates.lastKnownTokenStatus) {
227
- graph.lastKnownTokenStatus = updates.lastKnownTokenStatus;
228
- changed = true;
229
- }
230
- if (!changed)
231
- return;
232
- const output = { version: CONFIG_VERSION, graphs: config.graphs };
233
- await writeConfigFile(CONFIG_PATH, JSON.stringify(output, null, 2));
234
- }
235
- /**
236
- * Get all configured graphs (returns empty array if config doesn't exist)
237
- * Unlike getMcpConfig(), this doesn't throw if config is missing.
238
- */
239
- export async function getConfiguredGraphsSafe() {
240
- try {
241
- const config = await readRawConfig();
242
- return config.graphs;
243
- }
244
- catch {
245
- return [];
246
- }
247
- }
248
- // ============================================================================
249
- // Graph Lookup Functions
250
- // ============================================================================
251
- /**
252
- * Find a graph config by nickname (case-insensitive) or name
253
- */
254
- export async function findGraphConfig(nameOrNickname) {
255
- const config = await getMcpConfig();
256
- const lower = nameOrNickname.toLowerCase();
257
- // First try nickname (case-insensitive)
258
- const byNickname = config.graphs.find((g) => g.nickname.toLowerCase() === lower);
259
- if (byNickname)
260
- return byNickname;
261
- // Fall back to exact name match
262
- return config.graphs.find((g) => g.name === nameOrNickname);
263
- }
264
- /**
265
- * Get list of all configured graphs (for list_graphs tool and error messages)
266
- */
267
- export async function getConfiguredGraphs() {
268
- const config = await getMcpConfig();
269
- return config.graphs.map((g) => ({
270
- nickname: g.nickname,
271
- name: g.name,
272
- accessLevel: g.accessLevel || "full",
273
- ...(g.lastKnownTokenStatus ? { lastKnownTokenStatus: g.lastKnownTokenStatus } : {}),
274
- }));
275
- }
276
- // ============================================================================
277
- // Graph Resolution
278
- // ============================================================================
279
- /**
280
- * Resolve which graph to use and return full config.
281
- * Stateless: explicit param → single configured graph → error
282
- */
283
- export async function resolveGraph(providedGraph) {
284
- const config = await getMcpConfig();
285
- // 1. Explicit graph parameter (by nickname or name)
286
- if (providedGraph) {
287
- const graphConfig = await findGraphConfig(providedGraph);
288
- if (!graphConfig) {
289
- throw new RoamError(`Graph "${providedGraph}" not found in config. Available graph nicknames are listed below.`, ErrorCodes.GRAPH_NOT_CONFIGURED, {
290
- available_graphs: await getConfiguredGraphs(),
291
- instruction: "Pass the 'nickname' value as the graph parameter. After identifying which graph to use, ALWAYS call get_graph_guidelines next — it returns essential context and orientation for the session.",
292
- });
293
- }
294
- return {
295
- name: graphConfig.name,
296
- type: graphConfig.type,
297
- token: graphConfig.token,
298
- nickname: graphConfig.nickname,
299
- accessLevel: graphConfig.accessLevel,
300
- lastKnownTokenStatus: graphConfig.lastKnownTokenStatus,
301
- };
302
- }
303
- // 2. Auto-select if exactly one graph configured
304
- if (config.graphs.length === 1) {
305
- const graphConfig = config.graphs[0];
306
- return {
307
- name: graphConfig.name,
308
- type: graphConfig.type,
309
- token: graphConfig.token,
310
- nickname: graphConfig.nickname,
311
- accessLevel: graphConfig.accessLevel,
312
- lastKnownTokenStatus: graphConfig.lastKnownTokenStatus,
313
- };
314
- }
315
- // 3. Multiple graphs - require explicit selection
316
- throw new RoamError("Multiple graphs configured. Pass a graph nickname as the graph parameter to specify which graph to use.", ErrorCodes.GRAPH_NOT_SELECTED, {
317
- available_graphs: await getConfiguredGraphs(),
318
- instruction: "Pass the 'nickname' value as the graph parameter. After identifying which graph to use, ALWAYS call get_graph_guidelines next — it returns essential context and orientation for the session.",
319
- });
320
- }
321
- /**
322
- * Fetch list of open graphs from Roam's Local API.
323
- * Note: This is NOT used for graph resolution in v2.0.0.
324
- * It's kept for potential future use (e.g., showing which graphs are open).
325
- */
326
- export async function getOpenGraphs() {
327
- const port = await getPort();
328
- const url = `http://127.0.0.1:${port}/api/graphs/open`;
329
- const response = await fetch(url, {
330
- method: "GET",
331
- headers: { "Content-Type": "application/json" },
332
- });
333
- const data = (await response.json());
334
- if (!data.success) {
335
- throw new RoamError(data.error || "Failed to get open graphs", ErrorCodes.CONNECTION_FAILED);
336
- }
337
- return (data.result || []).map((g) => ({ name: g.name, type: g.type }));
338
- }
@@ -1,26 +0,0 @@
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
@@ -1 +0,0 @@
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"}
@@ -1,214 +0,0 @@
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. After identifying which graph to use, ALWAYS call get_graph_guidelines next — it returns essential context and orientation for the session.",
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
- }
@@ -1,32 +0,0 @@
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
@@ -1 +0,0 @@
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"}
package/dist/roam-api.js DELETED
@@ -1,50 +0,0 @@
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
- }