rootr-cli 0.2.1 → 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.
@@ -0,0 +1,110 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ import { z } from 'zod';
4
+
5
+ import { jsonResult, errorResult, makeClient, requireWorkspace } from './shared.js';
6
+ import { resolveTarget, requireResolved } from '../resolve.js';
7
+ import { RootrApiError } from '../client.js';
8
+
9
+ /** Reads file bytes from a local path or a base64 string; exactly one must be given. */
10
+ function readAttachmentBytes({ filePath, base64Data }) {
11
+ if (filePath) return readFileSync(filePath);
12
+ if (base64Data) return Buffer.from(base64Data, 'base64');
13
+ throw new RootrApiError('filePath 또는 base64Data 중 하나가 필요합니다.');
14
+ }
15
+
16
+ const uploadInputSchema = {
17
+ filename: z.string().describe('File name to store, e.g. "screenshot.png"'),
18
+ mimeType: z.string().describe('MIME type, e.g. "image/png"'),
19
+ filePath: z
20
+ .string()
21
+ .optional()
22
+ .describe('Local filesystem path to the file to upload (the MCP server reads this itself, e.g. "/tmp/screenshot.png")'),
23
+ base64Data: z
24
+ .string()
25
+ .optional()
26
+ .describe('Base64-encoded file bytes; use this when the caller has the file in memory instead of on disk'),
27
+ nodeId: z.string().optional().describe('Document/node id to associate the attachment with (permission-checked)'),
28
+ workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config'),
29
+ };
30
+
31
+ export function registerAttachmentTools(server) {
32
+ server.registerTool(
33
+ 'rootr_upload_attachment',
34
+ {
35
+ title: 'Upload a file/image as a Rootr attachment',
36
+ description:
37
+ 'Upload a local file (screenshot, photo, diagram export, ...) or base64 bytes to the Rootr (루터) ' +
38
+ 'workspace as an attachment, so it can be referenced in a document, presentation slide, or elsewhere. ' +
39
+ 'Give exactly one of filePath (read from the local filesystem) or base64Data. Returns ' +
40
+ '{ id, url, markdown, filename, mimeType, size } — `url` is the public, unauthenticated ' +
41
+ '/attachments/{id}/raw link (renders in <img> tags and on published /p/ documents without login), and ' +
42
+ '`markdown` is ready to paste into a document body (![alt](url)). For the common case of inserting an ' +
43
+ 'image straight into a document, prefer rootr_attach_image_to_document instead.',
44
+ annotations: { readOnlyHint: false, destructiveHint: false },
45
+ inputSchema: uploadInputSchema,
46
+ },
47
+ async ({ filename, mimeType, filePath, base64Data, nodeId, workspace }) => {
48
+ try {
49
+ const client = makeClient();
50
+ const ws = requireWorkspace(workspace);
51
+ const data = readAttachmentBytes({ filePath, base64Data });
52
+ const { id } = await client.uploadAttachment(ws, { filename, mimeType, nodeId, data });
53
+ const url = client.attachmentRawUrl(id);
54
+ return jsonResult({
55
+ id,
56
+ url,
57
+ markdown: `![${filename}](${url})`,
58
+ filename,
59
+ mimeType,
60
+ size: data.length,
61
+ });
62
+ } catch (err) {
63
+ return errorResult(err);
64
+ }
65
+ },
66
+ );
67
+
68
+ server.registerTool(
69
+ 'rootr_attach_image_to_document',
70
+ {
71
+ title: 'Upload an image and insert it into a document',
72
+ description:
73
+ 'Upload a local image (or base64 bytes) and append it — as `![alt](url)` markdown — into a Rootr (루터) ' +
74
+ 'document in one step. This is the tool to use when an AI is writing a report/spec/inspection log that ' +
75
+ 'needs a screenshot or photo embedded, since the web editor lets you drag-and-drop images but MCP callers ' +
76
+ 'otherwise cannot. Give exactly one of filePath or base64Data. Uses rootr_append semantics (conflict-free, ' +
77
+ 'additive) — pass underHeading to place it under a specific section.',
78
+ annotations: { readOnlyHint: false, destructiveHint: false },
79
+ inputSchema: {
80
+ target: z.string().describe('Document path (starting with "/") or node id to insert the image into'),
81
+ filename: uploadInputSchema.filename,
82
+ mimeType: uploadInputSchema.mimeType,
83
+ filePath: uploadInputSchema.filePath,
84
+ base64Data: uploadInputSchema.base64Data,
85
+ alt: z.string().optional().describe('Alt text / caption for the image (defaults to filename)'),
86
+ underHeading: z
87
+ .string()
88
+ .optional()
89
+ .describe('Heading to insert the image under, e.g. "## Screenshots". If omitted, appends to the document end.'),
90
+ workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config'),
91
+ },
92
+ },
93
+ async ({ target, filename, mimeType, filePath, base64Data, alt, underHeading, workspace }) => {
94
+ try {
95
+ const client = makeClient();
96
+ const ws = requireWorkspace(workspace);
97
+ const resolved = await resolveTarget(client, target);
98
+ const nodeId = requireResolved(resolved);
99
+ const data = readAttachmentBytes({ filePath, base64Data });
100
+ const { id } = await client.uploadAttachment(ws, { filename, mimeType, nodeId, data });
101
+ const url = client.attachmentRawUrl(id);
102
+ const markdown = `![${alt || filename}](${url})`;
103
+ const result = await client.appendDocument(nodeId, { content: markdown, underHeading });
104
+ return jsonResult({ id, url, markdown, appended: true, etag: result.etag });
105
+ } catch (err) {
106
+ return errorResult(err);
107
+ }
108
+ },
109
+ );
110
+ }