@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 +3 -0
- package/file.ts +161 -0
- package/index.ts +43 -0
- package/openclaw.plugin.json +12 -0
- package/package.json +24 -0
package/README.md
ADDED
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
|
+
}
|