roam-research-mcp 2.24.0 → 2.25.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
CHANGED
|
@@ -161,22 +161,24 @@ This is distinct from `CUSTOM_INSTRUCTIONS_PATH`, and the two compose:
|
|
|
161
161
|
| To change it | edit the file, restart the server | edit the page |
|
|
162
162
|
| Answers | how to write Roam markdown | how *this user* wants *this graph* handled |
|
|
163
163
|
|
|
164
|
-
**
|
|
164
|
+
**Just create the page.** With no configuration at all, `roam_get_guidelines` reads `[[roam/agent guidelines]]` — the same title Roam's own server reads, so writing it once makes both honour it. Creating a page with that exact namespaced title is the opt-in; nothing is read from the graph unless an agent explicitly calls the tool.
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
If the page doesn't exist, the tool returns `exists: false` rather than failing, so it is always safe to call.
|
|
167
|
+
|
|
168
|
+
Each graph can point at a different page, or turn it off:
|
|
167
169
|
|
|
168
170
|
```bash
|
|
169
171
|
ROAM_GRAPHS='{
|
|
170
|
-
"personal": {"token": "...", "graph": "..."
|
|
172
|
+
"personal": {"token": "...", "graph": "..."},
|
|
171
173
|
"work": {"token": "...", "graph": "...", "guidelinesPage": "work/agent rules"},
|
|
172
|
-
"
|
|
174
|
+
"private": {"token": "...", "graph": "...", "guidelinesPage": false}
|
|
173
175
|
}'
|
|
174
|
-
ROAM_GUIDELINES_PAGE='
|
|
176
|
+
ROAM_GUIDELINES_PAGE='team/agent guidelines' # change the default for every graph
|
|
175
177
|
```
|
|
176
178
|
|
|
177
|
-
Resolution order is **per-graph `guidelinesPage` → `ROAM_GUIDELINES_PAGE` →
|
|
179
|
+
Resolution order is **per-graph `guidelinesPage` → `ROAM_GUIDELINES_PAGE` → `roam/agent guidelines`**. Above: `personal` uses the env override, `work` uses its own page, and `private` has guidelines off entirely. **Only an explicit `false` disables it** — an unset value never does.
|
|
178
180
|
|
|
179
|
-
|
|
181
|
+
Results are cached for 30 seconds — an edit to the page takes effect without a restart. A starter template lives at [`.roam/agent-guidelines.template.md`](.roam/agent-guidelines.template.md).
|
|
180
182
|
|
|
181
183
|
Note that guidelines are read through the normal page path, so blocks tagged `#.rm-hide` / `#.rm-private` are withheld from them too — see below.
|
|
182
184
|
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { initializeGraph } from '@roam-research/roam-api-sdk';
|
|
11
11
|
import { RoamError } from '../shared/errors.js';
|
|
12
|
+
import { DEFAULT_GUIDELINES_PAGE } from '../tools/operations/guidelines.js';
|
|
12
13
|
/** List of tool names that perform write operations */
|
|
13
14
|
export const WRITE_OPERATIONS = [
|
|
14
15
|
'roam_create_page',
|
|
@@ -70,15 +71,21 @@ export class GraphRegistry {
|
|
|
70
71
|
return config?.memoriesTag ?? process.env.ROAM_MEMORIES_TAG ?? 'Memories';
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
73
|
-
* Page holding a graph's agent conventions, or null when disabled.
|
|
74
|
+
* Page holding a graph's agent conventions, or null when explicitly disabled.
|
|
74
75
|
*
|
|
75
|
-
* Precedence: per-graph config > ROAM_GUIDELINES_PAGE env var >
|
|
76
|
+
* Precedence: per-graph config > ROAM_GUIDELINES_PAGE env var >
|
|
77
|
+
* "roam/agent guidelines" (the shared convention).
|
|
76
78
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
79
|
+
* Reading the conventional title by default is deliberate. It is the same
|
|
80
|
+
* page Roam's own MCP server reads unconditionally, so writing it once makes
|
|
81
|
+
* both servers honour it — which is the entire point of a convention, and it
|
|
82
|
+
* stops working the moment it needs private configuration. Creating a page
|
|
83
|
+
* with that exact namespaced title IS the opt-in; nobody makes one by
|
|
84
|
+
* accident. And nothing here reads the graph unprompted: the lookup only
|
|
85
|
+
* happens when an agent explicitly calls roam_get_guidelines, so the tool
|
|
86
|
+
* call is already the consent.
|
|
87
|
+
*
|
|
88
|
+
* Set `guidelinesPage: false` to turn it off for a graph.
|
|
82
89
|
*/
|
|
83
90
|
getGuidelinesPage(key) {
|
|
84
91
|
const resolvedKey = key ?? this.defaultKey;
|
|
@@ -86,7 +93,7 @@ export class GraphRegistry {
|
|
|
86
93
|
if (config?.guidelinesPage === false) {
|
|
87
94
|
return null;
|
|
88
95
|
}
|
|
89
|
-
return config?.guidelinesPage ?? process.env.ROAM_GUIDELINES_PAGE ??
|
|
96
|
+
return config?.guidelinesPage ?? process.env.ROAM_GUIDELINES_PAGE ?? DEFAULT_GUIDELINES_PAGE;
|
|
90
97
|
}
|
|
91
98
|
/**
|
|
92
99
|
* Get an initialized Graph instance, creating it lazily if needed
|
|
@@ -67,10 +67,24 @@ describe('GraphRegistry', () => {
|
|
|
67
67
|
});
|
|
68
68
|
describe('getGuidelinesPage', () => {
|
|
69
69
|
const make = (configs, def = 'personal') => new GraphRegistry(configs, def);
|
|
70
|
-
it('
|
|
70
|
+
it('reads the shared convention when a graph configures nothing', () => {
|
|
71
|
+
// Creating a page with that exact namespaced title IS the opt-in, and it is
|
|
72
|
+
// the same title Roam's own server reads — so one page serves both.
|
|
71
73
|
delete process.env.ROAM_GUIDELINES_PAGE;
|
|
72
74
|
const r = make({ personal: { token: 't', graph: 'g' } });
|
|
73
|
-
expect(r.getGuidelinesPage('personal')).
|
|
75
|
+
expect(r.getGuidelinesPage('personal')).toBe('roam/agent guidelines');
|
|
76
|
+
});
|
|
77
|
+
it('requires an explicit false to turn off — nothing else disables it', () => {
|
|
78
|
+
delete process.env.ROAM_GUIDELINES_PAGE;
|
|
79
|
+
const off = make({ personal: { token: 't', graph: 'g', guidelinesPage: false } });
|
|
80
|
+
expect(off.getGuidelinesPage('personal')).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
it('false on one graph beats the env fallback', () => {
|
|
83
|
+
process.env.ROAM_GUIDELINES_PAGE = 'env/page';
|
|
84
|
+
const r = make({ personal: { token: 't', graph: 'p' }, work: { token: 't', graph: 'g', guidelinesPage: false } });
|
|
85
|
+
expect(r.getGuidelinesPage('work')).toBeNull();
|
|
86
|
+
expect(r.getGuidelinesPage('personal')).toBe('env/page');
|
|
87
|
+
delete process.env.ROAM_GUIDELINES_PAGE;
|
|
74
88
|
});
|
|
75
89
|
it('prefers per-graph config over the env var', () => {
|
|
76
90
|
process.env.ROAM_GUIDELINES_PAGE = 'env/page';
|
|
@@ -10,14 +10,11 @@
|
|
|
10
10
|
* server's mechanics and lives in a file. Guidelines are per-graph, live-edited
|
|
11
11
|
* from inside Roam, and answer "how does this user want their graph handled".
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
* With neither set the tool reports disabled and never touches the graph.
|
|
13
|
+
* Read by default; set `guidelinesPage: false` on a graph to disable it there.
|
|
15
14
|
*/
|
|
16
15
|
import { PageOperations } from './pages.js';
|
|
17
16
|
import { formatRoamDate } from '../../utils/helpers.js';
|
|
18
|
-
/** The
|
|
19
|
-
* automatically — a graph must opt in via `guidelinesPage` or
|
|
20
|
-
* ROAM_GUIDELINES_PAGE. */
|
|
17
|
+
/** The shared convention, read by default and by Roam's own MCP server. */
|
|
21
18
|
export const DEFAULT_GUIDELINES_PAGE = 'roam/agent guidelines';
|
|
22
19
|
/**
|
|
23
20
|
* Short TTL: the point of a page over a config file is that an edit takes
|
|
@@ -30,7 +27,7 @@ export class GuidelinesOperations {
|
|
|
30
27
|
/**
|
|
31
28
|
* @param guidelinesPage Page title to read, or null when disabled for this graph.
|
|
32
29
|
*/
|
|
33
|
-
constructor(graph, guidelinesPage =
|
|
30
|
+
constructor(graph, guidelinesPage = DEFAULT_GUIDELINES_PAGE) {
|
|
34
31
|
this.graph = graph;
|
|
35
32
|
this.guidelinesPage = guidelinesPage;
|
|
36
33
|
this.pageOps = new PageOperations(graph);
|
|
@@ -35,6 +35,13 @@ describe('GuidelinesOperations', () => {
|
|
|
35
35
|
expect(res.page).toBe('work/agent rules');
|
|
36
36
|
expect(getPageUid).toHaveBeenCalledWith('work/agent rules');
|
|
37
37
|
});
|
|
38
|
+
it('defaults to the shared convention with no page configured', async () => {
|
|
39
|
+
getPageUid.mockResolvedValue('abc123456');
|
|
40
|
+
fetchPageByTitle.mockResolvedValue('rules');
|
|
41
|
+
const res = await new GuidelinesOperations(newGraph()).getGuidelines();
|
|
42
|
+
expect(res.page).toBe(DEFAULT_GUIDELINES_PAGE);
|
|
43
|
+
expect(getPageUid).toHaveBeenCalledWith(DEFAULT_GUIDELINES_PAGE);
|
|
44
|
+
});
|
|
38
45
|
it('reports guidelines as disabled without touching the graph', async () => {
|
|
39
46
|
const res = await new GuidelinesOperations(newGraph(), null).getGuidelines();
|
|
40
47
|
expect(res.page).toBeNull();
|
|
@@ -9,12 +9,12 @@ import { MemoryOperations } from './operations/memory.js';
|
|
|
9
9
|
import { TodoOperations } from './operations/todos.js';
|
|
10
10
|
import { OutlineOperations } from './operations/outline.js';
|
|
11
11
|
import { BatchOperations } from './operations/batch.js';
|
|
12
|
-
import { GuidelinesOperations } from './operations/guidelines.js';
|
|
12
|
+
import { GuidelinesOperations, DEFAULT_GUIDELINES_PAGE } from './operations/guidelines.js';
|
|
13
13
|
import { TableOperations } from './operations/table.js';
|
|
14
14
|
import { DatomicSearchHandlerImpl } from './operations/search/handlers.js';
|
|
15
15
|
import { FullPageViewOperations } from './operations/full-page-view.js';
|
|
16
16
|
export class ToolHandlers {
|
|
17
|
-
constructor(graph, memoriesTag = 'Memories', guidelinesPage =
|
|
17
|
+
constructor(graph, memoriesTag = 'Memories', guidelinesPage = DEFAULT_GUIDELINES_PAGE) {
|
|
18
18
|
this.graph = graph;
|
|
19
19
|
this.cachedCheatsheet = null;
|
|
20
20
|
this.pageOps = new PageOperations(graph);
|