rootr-cli 0.1.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.
@@ -0,0 +1,115 @@
1
+ import { z } from 'zod';
2
+
3
+ import { jsonResult, errorResult, makeClient, requireWorkspace } from './shared.js';
4
+
5
+ const VALID_EVENTS = [
6
+ 'document.created',
7
+ 'document.updated',
8
+ 'document.moved',
9
+ 'document.deleted',
10
+ 'comment.created',
11
+ 'extraction.completed',
12
+ 'extraction.failed',
13
+ 'log.anomaly.rca',
14
+ 'scaffold.applied',
15
+ ];
16
+
17
+ /** Webhook management tools: list/create/delete. Requires webhooks:manage scope. */
18
+ export function registerWebhookTools(server) {
19
+ server.registerTool(
20
+ 'rootr_list_webhooks',
21
+ {
22
+ title: 'List webhooks configured on a workspace',
23
+ description:
24
+ 'List webhooks configured on a Rootr (루터) workspace. Secrets are not re-shown here (they only appear ' +
25
+ 'once, at creation time).',
26
+ inputSchema: {
27
+ workspace: z
28
+ .string()
29
+ .optional()
30
+ .describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
31
+ },
32
+ },
33
+ async ({ workspace }) => {
34
+ try {
35
+ const ws = requireWorkspace(workspace);
36
+ const client = makeClient(ws);
37
+ const result = await client.listWebhooks(ws);
38
+ return jsonResult(result);
39
+ } catch (err) {
40
+ return errorResult(err);
41
+ }
42
+ },
43
+ );
44
+
45
+ server.registerTool(
46
+ 'rootr_create_webhook',
47
+ {
48
+ title: 'Create a webhook',
49
+ description:
50
+ `Create a webhook on a Rootr (루터) workspace. Valid events: ${VALID_EVENTS.join(', ')}. ` +
51
+ 'Optionally scope it to a subtree (scopeNodeId, inherited by descendants) and/or a tag filter (filterTags). ' +
52
+ 'debounceSeconds coalesces bursts of events (server enforces a fixed trailing debounce with a cap). ' +
53
+ 'IMPORTANT: the response secret is shown ONLY ONCE, in this call\'s result — copy it now, it will not be ' +
54
+ 'shown again. LOOP GUARD: deliveries carry the actor as data.actorId ("apikey:<key-id>" for API writes) — ' +
55
+ 'a consumer MUST skip deliveries caused by its own key before reacting, or an agent that writes on ' +
56
+ 'notification will trigger itself forever.',
57
+ inputSchema: {
58
+ url: z.string().describe('HTTPS endpoint to receive webhook POSTs'),
59
+ events: z.array(z.string()).describe(`Event names to subscribe to, from: ${VALID_EVENTS.join(', ')}`),
60
+ name: z.string().optional().describe('Human-readable name for the webhook'),
61
+ scopeNodeId: z.string().optional().describe('Restrict to this node and its descendants'),
62
+ filterTags: z.array(z.string()).optional().describe('Only fire for nodes carrying one of these tags'),
63
+ debounceSeconds: z.number().optional().describe('Trailing debounce window in seconds (server-capped)'),
64
+ workspace: z
65
+ .string()
66
+ .optional()
67
+ .describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
68
+ },
69
+ },
70
+ async ({ url, events, name, scopeNodeId, filterTags, debounceSeconds, workspace }) => {
71
+ try {
72
+ const ws = requireWorkspace(workspace);
73
+ const client = makeClient(ws);
74
+ const result = await client.createWebhook(ws, {
75
+ name,
76
+ url,
77
+ events,
78
+ scopeNodeId,
79
+ filterTags,
80
+ debounceSeconds,
81
+ });
82
+ const note =
83
+ '\n\n(This secret will not be shown again — store it now if you need to verify signatures.)';
84
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) + note }] };
85
+ } catch (err) {
86
+ return errorResult(err);
87
+ }
88
+ },
89
+ );
90
+
91
+ server.registerTool(
92
+ 'rootr_delete_webhook',
93
+ {
94
+ title: 'Delete a webhook',
95
+ description: 'Delete a webhook from a Rootr (루터) workspace. This cannot be undone.',
96
+ inputSchema: {
97
+ webhookId: z.string().describe('Webhook id to delete'),
98
+ workspace: z
99
+ .string()
100
+ .optional()
101
+ .describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
102
+ },
103
+ },
104
+ async ({ webhookId, workspace }) => {
105
+ try {
106
+ const ws = requireWorkspace(workspace);
107
+ const client = makeClient(ws);
108
+ await client.deleteWebhook(ws, webhookId);
109
+ return jsonResult({ deleted: webhookId });
110
+ } catch (err) {
111
+ return errorResult(err);
112
+ }
113
+ },
114
+ );
115
+ }
@@ -0,0 +1,118 @@
1
+ import { z } from 'zod';
2
+
3
+ import { textResult, jsonResult, errorResult, makeClient, requireWorkspace } from './shared.js';
4
+
5
+ /** Workspace listing and scaffolding tools: rootr_workspaces, rootr_scaffold_plan, rootr_create_workspace, rootr_scaffold_apply. */
6
+ export function registerWorkspaceTools(server) {
7
+ server.registerTool(
8
+ 'rootr_workspaces',
9
+ {
10
+ title: 'List my Rootr workspaces',
11
+ description:
12
+ 'List the Rootr (루터) workspaces the current API key can see. Works with either key type: with an ' +
13
+ 'account key (PAT) this lists all workspaces you belong to; a workspace key only sees the workspace ' +
14
+ "it's scoped to. Use this to find a workspace id before calling other workspace-scoped tools.",
15
+ inputSchema: {},
16
+ },
17
+ async () => {
18
+ try {
19
+ const client = makeClient();
20
+ const workspaces = await client.listWorkspaces();
21
+ const rows = workspaces.map((w) => `${w.id}\t${w.name || w.slug || ''}`);
22
+ return textResult(rows.length ? rows.join('\n') : '(no workspaces visible to this key)');
23
+ } catch (err) {
24
+ return errorResult(err);
25
+ }
26
+ },
27
+ );
28
+
29
+ server.registerTool(
30
+ 'rootr_scaffold_plan',
31
+ {
32
+ title: 'Plan a workspace scaffold from an intent — then ASK THE USER before building',
33
+ description:
34
+ 'Step 1 of scaffolding, ALWAYS call this first. Given a natural-language intent (e.g. "SRE 온콜 위키" or ' +
35
+ '"sales CRM for a 5-person team"), Rootr\'s consultant returns a questionPolicy (ask the user MIN 3, up ' +
36
+ 'to 10 clarifying questions — this is not optional), the dimensions to cover, domain frameworks, and the ' +
37
+ 'buildingBlocks menu (DATABASE/DOCUMENT/SPREADSHEET/WHITEBOARD/FORM/ISSUE_TRACKER/FOLDER). You must then ' +
38
+ 'GENERATE your own questions from those frameworks, ask the user, and only design the tree AFTER their ' +
39
+ 'answers — every answer must visibly shape the result. Never build straight from a one-line intent. ' +
40
+ 'No workspace needed; create with rootr_create_workspace or rootr_scaffold_apply afterwards.',
41
+ inputSchema: {
42
+ intent: z.string().describe('Natural-language description of what the workspace/tree should be for'),
43
+ },
44
+ },
45
+ async ({ intent }) => {
46
+ try {
47
+ const client = makeClient();
48
+ const plan = await client.scaffoldPlan(intent);
49
+ return jsonResult(plan);
50
+ } catch (err) {
51
+ return errorResult(err);
52
+ }
53
+ },
54
+ );
55
+
56
+ server.registerTool(
57
+ 'rootr_create_workspace',
58
+ {
59
+ title: 'Create a brand-new Rootr workspace from a scaffold tree',
60
+ description:
61
+ 'Create an entirely new Rootr (루터) workspace pre-populated with the given document tree. ' +
62
+ 'REQUIRES an account key (PAT) with the workspaces:create scope — a workspace-scoped key cannot do this. ' +
63
+ 'PRECONDITION: call rootr_scaffold_plan first and ask the user its 3-10 clarifying questions; design the ' +
64
+ 'tree from their answers. Build a real tool, not a pile of thin markdown files: mix DATABASE (board/' +
65
+ 'timeline views, seed rows), SPREADSHEET (formulas), FORM, ISSUE_TRACKER and rich multi-section DOCUMENTs ' +
66
+ 'where they fit, and set an emoji `icon` on folders/databases/key documents. The `tree` shape follows ' +
67
+ 'the scaffold schema in llms.txt (max 200 nodes). AFTER creating: if your runtime can receive HTTP ' +
68
+ 'callbacks, offer the user to register a webhook on the new tree (rootr_create_webhook with scopeNodeId) ' +
69
+ 'so changes flow back to you — skip your own actorId to avoid self-triggering.',
70
+ inputSchema: {
71
+ name: z.string().describe('Name for the new workspace'),
72
+ intent: z.string().optional().describe('Original natural-language intent, for reference/regeneration'),
73
+ tree: z.any().describe('Scaffold tree to populate the new workspace with (see llms.txt tree schema)'),
74
+ },
75
+ },
76
+ async ({ name, intent, tree }) => {
77
+ try {
78
+ const client = makeClient();
79
+ const result = await client.createWorkspaceScaffold({ name, intent, tree });
80
+ return jsonResult(result);
81
+ } catch (err) {
82
+ return errorResult(err);
83
+ }
84
+ },
85
+ );
86
+
87
+ server.registerTool(
88
+ 'rootr_scaffold_apply',
89
+ {
90
+ title: 'Apply a scaffold tree to an existing workspace',
91
+ description:
92
+ 'Create the given document tree inside an existing Rootr (루터) workspace, optionally rooted under an ' +
93
+ 'existing path. Works with a workspace key (docs:write scope). PRECONDITION: call rootr_scaffold_plan ' +
94
+ 'first and ask the user its 3-10 clarifying questions; design the tree from their answers (same quality ' +
95
+ 'bar as rootr_create_workspace — real building blocks with icons/views/seed content, not thin markdown). ' +
96
+ 'AFTER creating: if your runtime can receive HTTP callbacks, offer a webhook on the new subtree ' +
97
+ '(rootr_create_webhook with scopeNodeId) so changes flow back to you — skip your own actorId.',
98
+ inputSchema: {
99
+ tree: z.any().describe('Scaffold tree to create (see llms.txt tree schema)'),
100
+ rootPath: z.string().optional().describe('Existing path to root the new tree under, e.g. "/projects"'),
101
+ workspace: z
102
+ .string()
103
+ .optional()
104
+ .describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
105
+ },
106
+ },
107
+ async ({ tree, rootPath, workspace }) => {
108
+ try {
109
+ const ws = requireWorkspace(workspace);
110
+ const client = makeClient(ws);
111
+ const result = await client.scaffoldApply(ws, { rootPath, tree });
112
+ return jsonResult(result);
113
+ } catch (err) {
114
+ return errorResult(err);
115
+ }
116
+ },
117
+ );
118
+ }
package/lib/mcp.js ADDED
@@ -0,0 +1,50 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+
4
+ import { registerDocumentTools } from './mcp-tools/documents.js';
5
+ import { registerWorkspaceTools } from './mcp-tools/workspaces.js';
6
+ import { registerLogTools } from './mcp-tools/logs.js';
7
+ import { registerAskTools } from './mcp-tools/ask.js';
8
+ import { registerIssueTools } from './mcp-tools/issues.js';
9
+ import { registerWebhookTools } from './mcp-tools/webhooks.js';
10
+ import { registerMiscTools } from './mcp-tools/misc.js';
11
+
12
+ export async function startMcpServer({ name, version }) {
13
+ const server = new McpServer(
14
+ { name, version },
15
+ {
16
+ instructions:
17
+ 'Rootr (pronounced "루터" in Korean — when the user says 루터, they mean Rootr) is the team\'s ' +
18
+ 'cloud document workspace. Use these tools whenever the user asks to read, write, append to, ' +
19
+ 'edit, or search their Rootr/루터 documents. Prefer rootr_append for adding content and ' +
20
+ 'rootr_edit for targeted changes; use rootr_write only for full rewrites. Beyond documents, this ' +
21
+ 'server also covers workspace scaffolding (planning/creating/applying document trees), typed LOG ' +
22
+ 'datastores (structured entries + stats), GraphRAG root-cause Q&A (rootr_ask), GitHub-style issue ' +
23
+ 'trackers, and webhooks.\n\n' +
24
+ 'Node types — pick the block that fits, never fake everything with markdown files: ' +
25
+ 'DOCUMENT = free-form markdown narrative (overviews, specs, decision logs); ' +
26
+ 'DATABASE = typed columns + views (table/board/calendar/timeline) for anything trackable — tasks, requirements, risks, milestones; ' +
27
+ 'SPREADSHEET = grid with formulas when content needs calculation (budgets, models, estimates); ' +
28
+ 'WHITEBOARD = freeform visual canvas (flow diagrams, brainstorming, journey maps); ' +
29
+ 'FORM = fillable intake from people outside the tree, optionally feeding a DATABASE (surveys, request queues); ' +
30
+ 'ISSUE_TRACKER = GitHub-style issues with #numbers, states, labels, comments (bug/work tracking with a lifecycle); ' +
31
+ 'FOLDER = grouping; LOG store = typed event table whose relation fields auto-build a lineage graph (metrics, incidents, pipeline events — use the rootr_*_log_* tools).\n\n' +
32
+ 'Scaffolding discipline: ALWAYS rootr_scaffold_plan first, ask the user the 3-10 clarifying questions it ' +
33
+ 'prescribes, and only then design and create the tree — every answer must visibly shape the structure. ' +
34
+ 'If your runtime can receive HTTP callbacks, offer to close the loop after scaffolding: register a webhook ' +
35
+ 'scoped to the new tree (rootr_create_webhook) so changes flow back to you — and ALWAYS skip deliveries ' +
36
+ 'whose actorId is your own key, or you will trigger yourself in an infinite loop.',
37
+ },
38
+ );
39
+
40
+ registerDocumentTools(server);
41
+ registerWorkspaceTools(server);
42
+ registerLogTools(server);
43
+ registerAskTools(server);
44
+ registerIssueTools(server);
45
+ registerWebhookTools(server);
46
+ registerMiscTools(server);
47
+
48
+ const transport = new StdioServerTransport();
49
+ await server.connect(transport);
50
+ }
package/lib/resolve.js ADDED
@@ -0,0 +1,30 @@
1
+ import { RootrApiError } from './client.js';
2
+
3
+ /**
4
+ * Resolve a CLI/MCP "target" argument to a node id.
5
+ * If it starts with '/', treat it as a path and look it up via by-path
6
+ * (requires a workspace to be configured). Otherwise treat it as an id already.
7
+ *
8
+ * Returns { id, existed, path } where existed indicates whether the path
9
+ * resolved to an existing node (always true for id-form targets, since we
10
+ * don't verify those against the API).
11
+ */
12
+ export async function resolveTarget(client, target) {
13
+ if (!target.startsWith('/')) {
14
+ return { id: target, existed: true, path: undefined };
15
+ }
16
+
17
+ const node = await client.getNodeByPath(target);
18
+ if (!node) {
19
+ return { id: undefined, existed: false, path: target };
20
+ }
21
+ const id = node.id || (node.node && node.node.id);
22
+ return { id, existed: true, path: target, node: node.node || node };
23
+ }
24
+
25
+ export function requireResolved(resolved) {
26
+ if (!resolved.existed || !resolved.id) {
27
+ throw new RootrApiError(`경로를 찾을 수 없습니다: ${resolved.path}`);
28
+ }
29
+ return resolved.id;
30
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "rootr-cli",
3
+ "version": "0.1.0",
4
+ "description": "Official Rootr CLI + MCP server — read, write, edit and search Rootr documents from the shell or from an AI agent.",
5
+ "type": "module",
6
+ "bin": {
7
+ "rootr": "bin/rootr.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "lib",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "rootr",
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "ai-agent",
22
+ "knowledge-base",
23
+ "markdown",
24
+ "cli"
25
+ ],
26
+ "homepage": "https://rootr.io",
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "@modelcontextprotocol/sdk": "^1.0.0",
30
+ "zod": "^3.23.8"
31
+ }
32
+ }