@2en/clawly-plugins 1.4.1 → 1.6.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/channel.ts CHANGED
@@ -9,8 +9,8 @@
9
9
 
10
10
  import {$} from 'zx'
11
11
  import type {PluginApi} from './index'
12
- import {sendPushNotification} from './notification'
13
- import {isClientOnline} from './presence'
12
+ import {sendPushNotification} from './gateway/notification'
13
+ import {isClientOnline} from './gateway/presence'
14
14
 
15
15
  $.verbose = false
16
16
 
@@ -3,7 +3,7 @@
3
3
  * invoking the LLM.
4
4
  */
5
5
 
6
- import type {PluginApi} from './index'
6
+ import type {PluginApi} from '../index'
7
7
 
8
8
  export function registerEchoCommand(api: PluginApi) {
9
9
  api.registerCommand({
@@ -0,0 +1,6 @@
1
+ import type {PluginApi} from '../index'
2
+ import {registerEchoCommand} from './clawly_echo'
3
+
4
+ export function registerCommands(api: PluginApi) {
5
+ registerEchoCommand(api)
6
+ }
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import {$} from 'zx'
12
- import type {PluginApi} from './index'
12
+ import type {PluginApi} from '../index'
13
13
  import {sendPushNotification} from './notification'
14
14
  import {isClientOnline} from './presence'
15
15
 
@@ -0,0 +1,68 @@
1
+ # clawhub2gateway
2
+
3
+ OpenClaw plugin that wraps the **ClawHub CLI** as **Gateway WebSocket RPC methods**.
4
+
5
+ ## What you get
6
+
7
+ Gateway methods (call via WS RPC):
8
+
9
+ - `clawhub2gateway.search`
10
+ - `clawhub2gateway.explore`
11
+ - `clawhub2gateway.install`
12
+ - `clawhub2gateway.update`
13
+ - `clawhub2gateway.list`
14
+ - `clawhub2gateway.inspect`
15
+ - `clawhub2gateway.star`
16
+ - `clawhub2gateway.unstar`
17
+
18
+ ## Install on a gateway host
19
+
20
+ 1) Install the ClawHub CLI (must be on PATH for the OpenClaw process):
21
+
22
+ ```bash
23
+ npm i -g clawhub
24
+ ```
25
+
26
+ 2) Put this plugin on the gateway host and make it discoverable. Common options:
27
+
28
+ - **Workspace plugin**: copy this folder to `<workspace>/.openclaw/extensions/clawhub2gateway/`
29
+ with `index.ts` + `openclaw.plugin.json`
30
+ - **Config path**: set `plugins.load.paths` to the plugin folder path
31
+
32
+ 3) Enable the plugin in your `openclaw.json`:
33
+
34
+ ```json5
35
+ {
36
+ "plugins": {
37
+ "enabled": true,
38
+ "entries": {
39
+ "clawhub2gateway": { "enabled": true, "config": {} }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ ## Plugin config
46
+
47
+ Configured under `plugins.entries.clawhub2gateway.config`:
48
+
49
+ - `bin` (string, default: `"clawhub"`): command to execute
50
+ - `defaultDir` (string, default: `"skills"`): default `--dir`
51
+ - `defaultNoInput` (boolean, default: `true`): default `--no-input`
52
+ - `defaultTimeoutMs` (number, default: `60000`)
53
+ - `configPath` (string, optional): sets `CLAWHUB_CONFIG_PATH`. Defaults to
54
+ `<OPENCLAW_STATE_DIR>/clawhub/config.json` when state dir is known.
55
+
56
+ ## Call examples (RPC params)
57
+
58
+ Install:
59
+
60
+ ```json
61
+ { "method": "clawhub2gateway.install", "params": { "slug": "my-skill-pack" } }
62
+ ```
63
+
64
+ Explore:
65
+
66
+ ```json
67
+ { "method": "clawhub2gateway.explore", "params": { "limit": 50, "sort": "trending", "json": true } }
68
+ ```
@@ -0,0 +1,405 @@
1
+ /**
2
+ * ClawHub CLI RPC bridge — expose ClawHub CLI commands as Gateway RPC methods.
3
+ *
4
+ * Gateway methods:
5
+ * - clawhub2gateway.search
6
+ * - clawhub2gateway.install
7
+ * - clawhub2gateway.update
8
+ * - clawhub2gateway.list
9
+ * - clawhub2gateway.explore
10
+ * - clawhub2gateway.inspect
11
+ * - clawhub2gateway.star
12
+ * - clawhub2gateway.unstar
13
+ *
14
+ * Notes:
15
+ * - This plugin shells out to the `clawhub` CLI. Install it on the gateway host:
16
+ * `npm i -g clawhub` (or ensure it is in PATH).
17
+ */
18
+
19
+ import fs from 'node:fs/promises'
20
+ import path from 'node:path'
21
+
22
+ import type {PluginApi} from '../index'
23
+
24
+ type JsonSchema = Record<string, unknown>
25
+
26
+ function isRecord(v: unknown): v is Record<string, unknown> {
27
+ return Boolean(v && typeof v === 'object' && !Array.isArray(v))
28
+ }
29
+
30
+ function readBool(params: Record<string, unknown>, key: string): boolean | undefined {
31
+ return typeof params[key] === 'boolean' ? (params[key] as boolean) : undefined
32
+ }
33
+
34
+ function readString(params: Record<string, unknown>, key: string): string | undefined {
35
+ const v = params[key]
36
+ if (typeof v !== 'string') return undefined
37
+ const t = v.trim()
38
+ return t ? t : undefined
39
+ }
40
+
41
+ function readNumber(params: Record<string, unknown>, key: string): number | undefined {
42
+ const v = params[key]
43
+ if (typeof v === 'number' && Number.isFinite(v)) return v
44
+ if (typeof v === 'string' && v.trim()) {
45
+ const n = Number(v.trim())
46
+ if (Number.isFinite(n)) return n
47
+ }
48
+ return undefined
49
+ }
50
+
51
+ function coercePluginConfig(api: PluginApi): Record<string, unknown> {
52
+ return isRecord(api.pluginConfig) ? api.pluginConfig : {}
53
+ }
54
+
55
+ function configString(
56
+ cfg: Record<string, unknown>,
57
+ key: string,
58
+ fallback?: string,
59
+ ): string | undefined {
60
+ const v = cfg[key]
61
+ if (typeof v === 'string' && v.trim()) return v.trim()
62
+ return fallback
63
+ }
64
+
65
+ function configBool(cfg: Record<string, unknown>, key: string, fallback: boolean): boolean {
66
+ const v = cfg[key]
67
+ if (typeof v === 'boolean') return v
68
+ return fallback
69
+ }
70
+
71
+ function configNumber(cfg: Record<string, unknown>, key: string, fallback: number): number {
72
+ const v = cfg[key]
73
+ if (typeof v === 'number' && Number.isFinite(v)) return v
74
+ return fallback
75
+ }
76
+
77
+ async function ensureDir(dirPath: string): Promise<void> {
78
+ await fs.mkdir(dirPath, {recursive: true})
79
+ }
80
+
81
+ function buildGlobalCliArgs(
82
+ params: Record<string, unknown>,
83
+ defaults: {dir: string; noInput: boolean},
84
+ ) {
85
+ const out: string[] = []
86
+
87
+ const workdir = readString(params, 'workdir')
88
+ const dir = readString(params, 'dir') ?? defaults.dir
89
+ const site = readString(params, 'site')
90
+ const registry = readString(params, 'registry')
91
+ const noInput = readBool(params, 'noInput') ?? defaults.noInput
92
+
93
+ if (workdir) out.push('--workdir', workdir)
94
+ if (dir) out.push('--dir', dir)
95
+ if (site) out.push('--site', site)
96
+ if (registry) out.push('--registry', registry)
97
+ if (noInput) out.push('--no-input')
98
+
99
+ return out
100
+ }
101
+
102
+ async function runClawhubCommand(opts: {
103
+ api: PluginApi
104
+ argv: string[]
105
+ cwd?: string
106
+ timeoutMs: number
107
+ env?: Record<string, string | undefined>
108
+ }) {
109
+ const runner = opts.api.runtime.system?.runCommandWithTimeout
110
+ if (!runner) {
111
+ throw new Error('Plugin runtime missing system.runCommandWithTimeout')
112
+ }
113
+ const result = await runner(opts.argv, {timeoutMs: opts.timeoutMs, cwd: opts.cwd, env: opts.env})
114
+ return result
115
+ }
116
+
117
+ function joinOutput(stdout: string, stderr: string): string {
118
+ const s1 = (stdout ?? '').trimEnd()
119
+ const s2 = (stderr ?? '').trimEnd()
120
+ if (s1 && s2) return `${s1}\n\n${s2}`
121
+ return s1 || s2 || ''
122
+ }
123
+
124
+ function baseToolParamsSchema(extra?: JsonSchema): JsonSchema {
125
+ return {
126
+ type: 'object',
127
+ additionalProperties: false,
128
+ properties: {
129
+ workdir: {type: 'string', description: 'Working directory (ClawHub --workdir)'},
130
+ dir: {type: 'string', description: 'Skills directory relative to workdir (ClawHub --dir)'},
131
+ site: {type: 'string', description: 'ClawHub site URL (ClawHub --site)'},
132
+ registry: {type: 'string', description: 'ClawHub registry API URL (ClawHub --registry)'},
133
+ noInput: {type: 'boolean', description: 'Disable prompts (ClawHub --no-input)'},
134
+ timeoutMs: {type: 'number', description: 'Command timeout in milliseconds'},
135
+ ...(extra ?? {}),
136
+ },
137
+ }
138
+ }
139
+
140
+ export function registerClawhub2gateway(api: PluginApi) {
141
+ const cfg = coercePluginConfig(api)
142
+ const bin = configString(cfg, 'bin', 'clawhub') ?? 'clawhub'
143
+ const defaultDir = configString(cfg, 'defaultDir', 'skills') ?? 'skills'
144
+ const defaultNoInput = configBool(cfg, 'defaultNoInput', true)
145
+ const defaultTimeoutMs = configNumber(cfg, 'defaultTimeoutMs', 60_000)
146
+ const configPath = configString(cfg, 'configPath')
147
+
148
+ const stateDir =
149
+ api.runtime.state?.resolveStateDir?.(process.env) ??
150
+ process.env.OPENCLAW_STATE_DIR ??
151
+ process.env.CLAWDBOT_STATE_DIR ??
152
+ ''
153
+ const defaultConfigPath = stateDir ? path.join(stateDir, 'clawhub', 'config.json') : ''
154
+
155
+ const resolveEnv = async (): Promise<Record<string, string | undefined>> => {
156
+ const env: Record<string, string | undefined> = {}
157
+ const resolved = configPath ?? defaultConfigPath
158
+ if (resolved) {
159
+ await ensureDir(path.dirname(resolved))
160
+ env.CLAWHUB_CONFIG_PATH = resolved
161
+ }
162
+ return env
163
+ }
164
+
165
+ const registerRpc = (rpc: {
166
+ method: string
167
+ description: string
168
+ parameters: JsonSchema
169
+ buildCli: (
170
+ params: Record<string, unknown>,
171
+ ) => Promise<{argv: string[]; cwd?: string; timeoutMs?: number}>
172
+ }) => {
173
+ api.registerGatewayMethod(rpc.method, async ({params, respond}) => {
174
+ try {
175
+ const timeoutMs = Math.max(
176
+ 1_000,
177
+ Math.floor(readNumber(params, 'timeoutMs') ?? defaultTimeoutMs),
178
+ )
179
+ const built = await rpc.buildCli(params)
180
+ const env = await resolveEnv()
181
+ const res = await runClawhubCommand({
182
+ api,
183
+ argv: built.argv,
184
+ cwd: built.cwd,
185
+ timeoutMs: built.timeoutMs ?? timeoutMs,
186
+ env,
187
+ })
188
+ const output = joinOutput(res.stdout, res.stderr)
189
+ if (res.code && res.code !== 0) {
190
+ respond(false, undefined, {
191
+ code: 'command_failed',
192
+ message: output || `clawhub exited with code ${res.code}`,
193
+ })
194
+ return
195
+ }
196
+ respond(true, {
197
+ ok: true,
198
+ output,
199
+ stdout: res.stdout,
200
+ stderr: res.stderr,
201
+ code: res.code,
202
+ signal: res.signal,
203
+ killed: res.killed,
204
+ argv: built.argv,
205
+ cwd: built.cwd,
206
+ })
207
+ } catch (err) {
208
+ respond(false, undefined, {
209
+ code: 'error',
210
+ message: err instanceof Error ? err.message : String(err),
211
+ })
212
+ }
213
+ })
214
+ }
215
+
216
+ // -----------------------------
217
+ // Search
218
+ // -----------------------------
219
+
220
+ registerRpc({
221
+ method: 'clawhub2gateway.search',
222
+ description: 'ClawHub CLI: search skills.',
223
+ parameters: baseToolParamsSchema({
224
+ query: {type: 'string', description: 'Search query'},
225
+ limit: {type: 'number', description: 'Max results (clawhub search --limit)'},
226
+ }),
227
+ async buildCli(params) {
228
+ const query = readString(params, 'query')
229
+ if (!query) throw new Error('query required')
230
+ const limit = readNumber(params, 'limit')
231
+ const globalArgs = buildGlobalCliArgs(params, {dir: defaultDir, noInput: defaultNoInput})
232
+ const argv = [bin, 'search', query, ...globalArgs]
233
+ if (limit !== undefined) argv.push('--limit', String(Math.max(1, Math.floor(limit))))
234
+ return {argv}
235
+ },
236
+ })
237
+
238
+ registerRpc({
239
+ method: 'clawhub2gateway.explore',
240
+ description: 'ClawHub CLI: explore skills.!',
241
+ parameters: baseToolParamsSchema({
242
+ limit: {type: 'number', description: 'Max results (clawhub explore --limit, if supported)'},
243
+ sort: {
244
+ type: 'string',
245
+ enum: ['newest', 'downloads', 'rating', 'installs', 'installsAllTime', 'trending'],
246
+ description:
247
+ 'Sort order (clawhub explore --sort). One of newest, downloads, rating, installs, installsAllTime, trending.',
248
+ },
249
+ json: {type: 'boolean', description: 'Output JSON'},
250
+ }),
251
+ async buildCli(params) {
252
+ const limit = readNumber(params, 'limit')
253
+ const sort = readString(params, 'sort')
254
+ const json = readBool(params, 'json') ?? false
255
+ const globalArgs = buildGlobalCliArgs(params, {dir: defaultDir, noInput: defaultNoInput})
256
+ const argv = [bin, 'explore', ...globalArgs]
257
+ if (limit !== undefined) argv.push('--limit', String(Math.max(1, Math.floor(limit))))
258
+ if (sort) argv.push('--sort', sort)
259
+ if (json) argv.push('--json')
260
+ return {argv}
261
+ },
262
+ })
263
+
264
+ // -----------------------------
265
+ // Install / Update / List
266
+ // -----------------------------
267
+
268
+ registerRpc({
269
+ method: 'clawhub2gateway.install',
270
+ description: 'ClawHub CLI: install a skill into the workspace skills dir.',
271
+ parameters: baseToolParamsSchema({
272
+ slug: {type: 'string', description: 'Skill slug to install'},
273
+ version: {type: 'string', description: 'Specific version to install'},
274
+ force: {
275
+ type: 'boolean',
276
+ description: 'Overwrite if folder exists (clawhub install --force)',
277
+ },
278
+ }),
279
+ async buildCli(params) {
280
+ const slug = readString(params, 'slug')
281
+ if (!slug) throw new Error('slug required')
282
+ const version = readString(params, 'version')
283
+ const force = readBool(params, 'force') ?? false
284
+ const globalArgs = buildGlobalCliArgs(params, {dir: defaultDir, noInput: defaultNoInput})
285
+ const argv = [bin, 'install', slug, ...globalArgs]
286
+ if (version) argv.push('--version', version)
287
+ if (force) argv.push('--force')
288
+ return {argv}
289
+ },
290
+ })
291
+
292
+ registerRpc({
293
+ method: 'clawhub2gateway.update',
294
+ description: 'ClawHub CLI: update one skill or all installed skills.',
295
+ parameters: baseToolParamsSchema({
296
+ slug: {type: 'string', description: 'Skill slug to update (omit when all=true)'},
297
+ all: {type: 'boolean', description: 'Update all skills (clawhub update --all)'},
298
+ version: {type: 'string', description: 'Update to a specific version (single slug only)'},
299
+ force: {
300
+ type: 'boolean',
301
+ description: 'Overwrite when local files do not match any published version',
302
+ },
303
+ }),
304
+ async buildCli(params) {
305
+ const all = readBool(params, 'all') ?? false
306
+ const slug = readString(params, 'slug')
307
+ const version = readString(params, 'version')
308
+ const force = readBool(params, 'force') ?? false
309
+ const globalArgs = buildGlobalCliArgs(params, {dir: defaultDir, noInput: defaultNoInput})
310
+
311
+ const argv = [bin, 'update', ...globalArgs]
312
+ if (all) {
313
+ argv.push('--all')
314
+ } else {
315
+ if (!slug) throw new Error('slug required when all=false')
316
+ argv.push(slug)
317
+ if (version) argv.push('--version', version)
318
+ }
319
+ if (force) argv.push('--force')
320
+ return {argv}
321
+ },
322
+ })
323
+
324
+ registerRpc({
325
+ method: 'clawhub2gateway.list',
326
+ description: 'ClawHub CLI: list installed skills from .clawhub/lock.json.',
327
+ parameters: baseToolParamsSchema(),
328
+ async buildCli(params) {
329
+ const argv = [
330
+ bin,
331
+ 'list',
332
+ ...buildGlobalCliArgs(params, {dir: defaultDir, noInput: defaultNoInput}),
333
+ ]
334
+ return {argv}
335
+ },
336
+ })
337
+
338
+ // -----------------------------
339
+ // Inspect / Star
340
+ // -----------------------------
341
+
342
+ registerRpc({
343
+ method: 'clawhub2gateway.inspect',
344
+ description: 'ClawHub CLI: inspect a skill without installing.',
345
+ parameters: baseToolParamsSchema({
346
+ slug: {type: 'string', description: 'Skill slug'},
347
+ }),
348
+ async buildCli(params) {
349
+ const slug = readString(params, 'slug')
350
+ if (!slug) throw new Error('slug required')
351
+ const globalArgs = buildGlobalCliArgs(params, {dir: defaultDir, noInput: defaultNoInput})
352
+
353
+ const argv = [bin, 'inspect', slug, ...globalArgs]
354
+ return {argv}
355
+ },
356
+ })
357
+
358
+ registerRpc({
359
+ method: 'clawhub2gateway.star',
360
+ description: 'ClawHub CLI: star a skill.',
361
+ parameters: baseToolParamsSchema({
362
+ slug: {type: 'string', description: 'Skill slug'},
363
+ }),
364
+ async buildCli(params) {
365
+ const slug = readString(params, 'slug')
366
+ if (!slug) throw new Error('slug required')
367
+ const argv = [
368
+ bin,
369
+ 'star',
370
+ slug,
371
+ ...buildGlobalCliArgs(params, {dir: defaultDir, noInput: defaultNoInput}),
372
+ ]
373
+ return {argv}
374
+ },
375
+ })
376
+
377
+ registerRpc({
378
+ method: 'clawhub2gateway.unstar',
379
+ description: 'ClawHub CLI: unstar a skill.',
380
+ parameters: baseToolParamsSchema({
381
+ slug: {type: 'string', description: 'Skill slug'},
382
+ }),
383
+ async buildCli(params) {
384
+ const slug = readString(params, 'slug')
385
+ if (!slug) throw new Error('slug required')
386
+ const argv = [
387
+ bin,
388
+ 'unstar',
389
+ slug,
390
+ ...buildGlobalCliArgs(params, {dir: defaultDir, noInput: defaultNoInput}),
391
+ ]
392
+ return {argv}
393
+ },
394
+ })
395
+
396
+ api.logger.info(
397
+ [
398
+ `clawhub2gateway: registered.`,
399
+ `Using clawhub bin: ${bin}`,
400
+ defaultConfigPath
401
+ ? `CLI config: ${configPath ?? defaultConfigPath}`
402
+ : 'CLI config: (not set)',
403
+ ].join(' '),
404
+ )
405
+ }
@@ -0,0 +1,16 @@
1
+ import type {PluginApi} from '../index'
2
+ import {registerAgentSend} from './agent'
3
+ import {registerClawhub2gateway} from './clawhub2gateway'
4
+ import {registerMemoryBrowser} from './memory'
5
+ import {registerNotification} from './notification'
6
+ import {registerPlugins} from './plugins'
7
+ import {registerPresence} from './presence'
8
+
9
+ export function registerGateway(api: PluginApi) {
10
+ registerPresence(api)
11
+ registerNotification(api)
12
+ registerAgentSend(api)
13
+ registerMemoryBrowser(api)
14
+ registerClawhub2gateway(api)
15
+ registerPlugins(api)
16
+ }
@@ -0,0 +1,55 @@
1
+ # memory-browser
2
+
3
+ OpenClaw plugin that exposes the **memory directory** (all `.md` files) as **Gateway WebSocket RPC methods**.
4
+
5
+ ## What you get
6
+
7
+ Gateway methods (call via WS RPC):
8
+
9
+ - **`memory-browser.list`** — List all `.md` files in the memory directory (recursive). Returns `{ files: string[] }` (relative paths).
10
+ - **`memory-browser.get`** — Return the content of a single `.md` file. Params: `{ path: string }` (relative path, e.g. `"notes/foo.md"`). Returns `{ path: string, content: string }`.
11
+
12
+ ## Memory directory
13
+
14
+ The memory directory is resolved in this order:
15
+
16
+ 1. Plugin config `memoryDir` (absolute path)
17
+ 2. `<OPENCLAW_STATE_DIR>/memory` (or `<CLAWDBOT_STATE_DIR>/memory`)
18
+ 3. `<process.cwd()>/memory`
19
+
20
+ ## Install on a gateway host
21
+
22
+ 1. Put this plugin on the gateway host and make it discoverable, e.g.:
23
+ - Workspace: `<workspace>/.openclaw/extensions/memory-browser/` with `index.ts` + `openclaw.plugin.json`
24
+ - Or set `plugins.load.paths` to the plugin folder
25
+
26
+ 2. Enable the plugin in `openclaw.json`:
27
+
28
+ ```json5
29
+ {
30
+ "plugins": {
31
+ "enabled": true,
32
+ "entries": {
33
+ "memory-browser": { "enabled": true, "config": {} }
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ Optional config: `config.memoryDir` to override the memory directory path.
40
+
41
+ ## Call examples (RPC params)
42
+
43
+ List all `.md` files:
44
+
45
+ ```json
46
+ { "method": "memory-browser.list", "params": {} }
47
+ ```
48
+
49
+ Get one file:
50
+
51
+ ```json
52
+ { "method": "memory-browser.get", "params": { "path": "notes/meeting-2024.md" } }
53
+ ```
54
+
55
+ `path` must be a relative path to a `.md` file inside the memory directory; directory traversal (`..`) is rejected.