@2en/clawly-plugins 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.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @2en/clawly-plugins
2
+
3
+ OpenClaw plugin providing Clawly utility RPC methods (`clawly.*`).
package/file.ts ADDED
@@ -0,0 +1,161 @@
1
+ /**
2
+ * clawly.file.get — read a file from the local filesystem and return it as base64.
3
+ *
4
+ * Validates: absolute path, no traversal, MIME whitelist, 50 MB size limit.
5
+ */
6
+
7
+ import fs from 'node:fs/promises'
8
+ import path from 'node:path'
9
+ import {fileTypeFromBuffer} from 'file-type'
10
+
11
+ import type {PluginApi} from './index'
12
+
13
+ const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50 MB
14
+
15
+ const ALLOWED_MIME_PREFIXES = ['image/', 'video/', 'audio/']
16
+
17
+ /** Extensions allowed via fallback when file-type cannot detect MIME (e.g. plain text). */
18
+ const DOCUMENT_EXT_WHITELIST = new Set([
19
+ 'pdf',
20
+ 'doc',
21
+ 'docx',
22
+ 'xls',
23
+ 'xlsx',
24
+ 'ppt',
25
+ 'pptx',
26
+ 'txt',
27
+ 'csv',
28
+ 'rtf',
29
+ ])
30
+
31
+ const EXT_TO_MIME: Record<string, string> = {
32
+ pdf: 'application/pdf',
33
+ doc: 'application/msword',
34
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
35
+ xls: 'application/vnd.ms-excel',
36
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
37
+ ppt: 'application/vnd.ms-powerpoint',
38
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
39
+ txt: 'text/plain',
40
+ csv: 'text/csv',
41
+ rtf: 'application/rtf',
42
+ }
43
+
44
+ function getExtension(filePath: string): string {
45
+ const dot = filePath.lastIndexOf('.')
46
+ return dot === -1 ? '' : filePath.slice(dot + 1).toLowerCase()
47
+ }
48
+
49
+ function getFilename(filePath: string): string {
50
+ return path.basename(filePath)
51
+ }
52
+
53
+ export function registerFileMethods(api: PluginApi) {
54
+ api.registerGatewayMethod('clawly.file.get', async ({params, respond}) => {
55
+ const rawPath = typeof params.path === 'string' ? params.path.trim() : ''
56
+
57
+ // ── Validate path ──────────────────────────────────────────────
58
+ if (!rawPath) {
59
+ respond(false, undefined, {code: 'invalid_params', message: 'path is required'})
60
+ return
61
+ }
62
+
63
+ if (!path.isAbsolute(rawPath)) {
64
+ respond(false, undefined, {code: 'invalid_params', message: 'path must be absolute'})
65
+ return
66
+ }
67
+
68
+ const normalized = path.normalize(rawPath)
69
+
70
+ // Resolve symlinks and verify the real path matches the normalized expectation
71
+ let realPath: string
72
+ try {
73
+ realPath = await fs.realpath(normalized)
74
+ } catch (err) {
75
+ const code = (err as NodeJS.ErrnoException).code
76
+ if (code === 'ENOENT') {
77
+ respond(false, undefined, {code: 'not_found', message: 'file not found'})
78
+ return
79
+ }
80
+ api.logger.error(
81
+ `clawly.file.get realpath failed: ${err instanceof Error ? err.message : String(err)}`,
82
+ )
83
+ respond(false, undefined, {code: 'error', message: 'failed to resolve path'})
84
+ return
85
+ }
86
+
87
+ // ── Size check ─────────────────────────────────────────────────
88
+ let stat: Awaited<ReturnType<typeof fs.stat>>
89
+ try {
90
+ stat = await fs.stat(realPath)
91
+ } catch (err) {
92
+ const code = (err as NodeJS.ErrnoException).code
93
+ if (code === 'ENOENT') {
94
+ respond(false, undefined, {code: 'not_found', message: 'file not found'})
95
+ return
96
+ }
97
+ api.logger.error(
98
+ `clawly.file.get stat failed: ${err instanceof Error ? err.message : String(err)}`,
99
+ )
100
+ respond(false, undefined, {code: 'error', message: 'failed to stat file'})
101
+ return
102
+ }
103
+
104
+ if (!stat.isFile()) {
105
+ respond(false, undefined, {code: 'invalid_params', message: 'path is not a regular file'})
106
+ return
107
+ }
108
+
109
+ if (stat.size > MAX_FILE_SIZE) {
110
+ respond(false, undefined, {
111
+ code: 'file_too_large',
112
+ message: `file exceeds ${MAX_FILE_SIZE / 1024 / 1024} MB limit`,
113
+ })
114
+ return
115
+ }
116
+
117
+ // ── Read file ──────────────────────────────────────────────────
118
+ let buffer: Buffer
119
+ try {
120
+ buffer = await fs.readFile(realPath)
121
+ } catch (err) {
122
+ api.logger.error(
123
+ `clawly.file.get readFile failed: ${err instanceof Error ? err.message : String(err)}`,
124
+ )
125
+ respond(false, undefined, {code: 'error', message: 'failed to read file'})
126
+ return
127
+ }
128
+
129
+ // ── MIME detection ─────────────────────────────────────────────
130
+ let mimeType: string | undefined
131
+
132
+ const detected = await fileTypeFromBuffer(buffer)
133
+ if (detected) {
134
+ mimeType = detected.mime
135
+ }
136
+
137
+ // Check against allowed MIME prefixes
138
+ if (mimeType && ALLOWED_MIME_PREFIXES.some((prefix) => mimeType!.startsWith(prefix))) {
139
+ // Allowed media type
140
+ } else {
141
+ // Fallback: check document extension whitelist
142
+ const ext = getExtension(realPath)
143
+ if (DOCUMENT_EXT_WHITELIST.has(ext)) {
144
+ mimeType = EXT_TO_MIME[ext] ?? 'application/octet-stream'
145
+ } else {
146
+ respond(false, undefined, {
147
+ code: 'unsupported_type',
148
+ message: `file type not allowed: ${mimeType ?? `unknown (ext: .${ext || '?'})`}`,
149
+ })
150
+ return
151
+ }
152
+ }
153
+
154
+ // ── Respond ────────────────────────────────────────────────────
155
+ const base64 = buffer.toString('base64')
156
+ const filename = getFilename(realPath)
157
+
158
+ api.logger.info(`clawly.file.get: served ${filename} (${mimeType}, ${buffer.length} bytes)`)
159
+ respond(true, {base64, mimeType, filename})
160
+ })
161
+ }
package/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * OpenClaw plugin: Clawly utility RPC methods (clawly.*).
3
+ *
4
+ * Gateway methods:
5
+ * - clawly.file.get — read a local file and return base64-encoded content
6
+ */
7
+
8
+ import {registerFileMethods} from './file'
9
+
10
+ type PluginRuntime = {
11
+ state?: {
12
+ resolveStateDir?: (env?: NodeJS.ProcessEnv) => string
13
+ }
14
+ }
15
+
16
+ export type PluginApi = {
17
+ id: string
18
+ name: string
19
+ pluginConfig?: Record<string, unknown>
20
+ logger: {
21
+ info: (msg: string) => void
22
+ warn: (msg: string) => void
23
+ error: (msg: string) => void
24
+ }
25
+ runtime: PluginRuntime
26
+ registerGatewayMethod: (
27
+ method: string,
28
+ handler: (opts: {
29
+ params: Record<string, unknown>
30
+ respond: (ok: boolean, payload?: unknown, error?: {code?: string; message?: string}) => void
31
+ }) => Promise<void> | void,
32
+ ) => void
33
+ }
34
+
35
+ export default {
36
+ id: 'clawly-plugins',
37
+ name: 'Clawly Plugins',
38
+ description: 'Clawly utility RPC methods (clawly.*).',
39
+ register(api: PluginApi) {
40
+ registerFileMethods(api)
41
+ api.logger.info(`Loaded ${api.id} plugin.`)
42
+ },
43
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "clawly-plugins",
3
+ "name": "Clawly Plugins",
4
+ "description": "Clawly utility RPC methods (clawly.*): file access.",
5
+ "version": "0.1.0",
6
+ "configSchema": {
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "properties": {},
10
+ "required": []
11
+ }
12
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@2en/clawly-plugins",
3
+ "version": "0.1.0",
4
+ "module": "index.ts",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/2enai/clawly",
9
+ "directory": "plugins/clawly-plugins"
10
+ },
11
+ "files": [
12
+ "index.ts",
13
+ "file.ts",
14
+ "openclaw.plugin.json"
15
+ ],
16
+ "dependencies": {
17
+ "file-type": "^19.6.0"
18
+ },
19
+ "openclaw": {
20
+ "extensions": [
21
+ "./index.ts"
22
+ ]
23
+ }
24
+ }