@2en/clawly-plugins 1.21.6 → 1.22.1
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/gateway/index.ts +2 -0
- package/gateway/plugins.ts +9 -10
- package/gateway/version.ts +44 -0
- package/index.ts +89 -23
- package/lib/stripCliLogs.test.ts +12 -3
- package/lib/stripCliLogs.ts +9 -6
- package/package.json +1 -1
package/gateway/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {registerNotification} from './notification'
|
|
|
8
8
|
import {registerOfflinePush} from './offline-push'
|
|
9
9
|
import {registerPlugins} from './plugins'
|
|
10
10
|
import {registerPresence} from './presence'
|
|
11
|
+
import {registerVersion} from './version'
|
|
11
12
|
|
|
12
13
|
export function registerGateway(api: PluginApi) {
|
|
13
14
|
registerPresence(api)
|
|
@@ -19,4 +20,5 @@ export function registerGateway(api: PluginApi) {
|
|
|
19
20
|
registerOfflinePush(api)
|
|
20
21
|
registerConfigRepair(api)
|
|
21
22
|
registerPairing(api)
|
|
23
|
+
registerVersion(api)
|
|
22
24
|
}
|
package/gateway/plugins.ts
CHANGED
|
@@ -72,19 +72,19 @@ async function withPluginBackup<T>(
|
|
|
72
72
|
if (!stateDir) return operation()
|
|
73
73
|
|
|
74
74
|
const extensionDir = path.join(stateDir, 'extensions', pluginId)
|
|
75
|
-
const
|
|
75
|
+
const backupZip = `${extensionDir}.update-backup.zip`
|
|
76
76
|
|
|
77
77
|
// Back up only if there's a valid existing installation.
|
|
78
78
|
// If backup fails, abort — a working plugin is more valuable than a risky update.
|
|
79
79
|
const hasExisting = fs.existsSync(path.join(extensionDir, PLUGIN_SENTINEL))
|
|
80
80
|
if (hasExisting) {
|
|
81
81
|
try {
|
|
82
|
-
if (fs.existsSync(
|
|
83
|
-
|
|
82
|
+
if (fs.existsSync(backupZip)) fs.unlinkSync(backupZip)
|
|
83
|
+
const extDir = path.join(stateDir, 'extensions')
|
|
84
|
+
await $({cwd: extDir})`zip -rqy ${backupZip} ${pluginId}`
|
|
84
85
|
api.logger.info(`plugins: backed up ${pluginId}`)
|
|
85
86
|
} catch (err) {
|
|
86
|
-
|
|
87
|
-
if (fs.existsSync(backupDir)) await $`rm -rf ${backupDir}`.catch(() => {})
|
|
87
|
+
if (fs.existsSync(backupZip)) fs.unlinkSync(backupZip)
|
|
88
88
|
throw new Error(
|
|
89
89
|
`plugin backup failed, aborting update: ${err instanceof Error ? err.message : String(err)}`,
|
|
90
90
|
)
|
|
@@ -104,17 +104,16 @@ async function withPluginBackup<T>(
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// Clean backup on success
|
|
107
|
-
if (fs.existsSync(
|
|
108
|
-
await $`rm -rf ${backupDir}`.catch(() => {})
|
|
109
|
-
}
|
|
107
|
+
if (fs.existsSync(backupZip)) fs.unlinkSync(backupZip)
|
|
110
108
|
|
|
111
109
|
return result
|
|
112
110
|
} catch (err) {
|
|
113
|
-
if (fs.existsSync(
|
|
111
|
+
if (fs.existsSync(backupZip)) {
|
|
114
112
|
api.logger.warn(`plugins: rolling back ${pluginId} from backup`)
|
|
115
113
|
try {
|
|
116
114
|
if (fs.existsSync(extensionDir)) await $`rm -rf ${extensionDir}`
|
|
117
|
-
await $`
|
|
115
|
+
await $`unzip -qo ${backupZip} -d ${path.join(stateDir, 'extensions')}`
|
|
116
|
+
fs.unlinkSync(backupZip)
|
|
118
117
|
api.logger.info(`plugins: rollback complete`)
|
|
119
118
|
} catch (rollbackErr) {
|
|
120
119
|
api.logger.error(
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw version check — runs `openclaw --version` and returns the result.
|
|
3
|
+
*
|
|
4
|
+
* Method: clawly.version({}) → { version: string }
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {PluginApi} from '../index'
|
|
8
|
+
|
|
9
|
+
export function registerVersion(api: PluginApi) {
|
|
10
|
+
api.registerGatewayMethod('clawly.version', async ({respond}) => {
|
|
11
|
+
const run = api.runtime.system?.runCommandWithTimeout
|
|
12
|
+
if (!run) {
|
|
13
|
+
respond(false, undefined, {
|
|
14
|
+
code: 'no_runtime',
|
|
15
|
+
message: 'runCommandWithTimeout not available',
|
|
16
|
+
})
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const result = await run(['openclaw', '--version'], {timeoutMs: 5000})
|
|
22
|
+
if (result.code !== 0) {
|
|
23
|
+
const message = result.killed
|
|
24
|
+
? `Process killed${result.signal ? ` by ${result.signal}` : ''}`
|
|
25
|
+
: `Exited with code ${result.code}`
|
|
26
|
+
respond(false, undefined, {code: 'non_zero_exit', message})
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
const raw = (result.stdout + result.stderr).trim()
|
|
30
|
+
// openclaw --version typically outputs something like "openclaw/2026.2.24 ..."
|
|
31
|
+
// Extract just the version portion
|
|
32
|
+
const match = raw.match(/(\d{4}\.\d+\.\d+)/)
|
|
33
|
+
const version = match ? match[1] : raw
|
|
34
|
+
respond(true, {version})
|
|
35
|
+
} catch (err) {
|
|
36
|
+
respond(false, undefined, {
|
|
37
|
+
code: 'exec_failed',
|
|
38
|
+
message: err instanceof Error ? err.message : 'Failed to get version',
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
api.logger.info('version: registered clawly.version method')
|
|
44
|
+
}
|
package/index.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - clawly.notification.send — send a push notification directly
|
|
9
9
|
* - clawly.agent.send — trigger agent turn with a message (+ optional push)
|
|
10
10
|
* - clawly.agent.echo — echo-wrapped agent message (bypasses LLM)
|
|
11
|
+
* - clawly.version — get the OpenClaw server version
|
|
11
12
|
* - memory-browser.list — list all .md files in the memory directory
|
|
12
13
|
* - memory-browser.get — return content of a single .md file
|
|
13
14
|
* - clawhub2gateway.* — ClawHub CLI RPC bridge (search/install/update/list/explore/inspect/star/unstar)
|
|
@@ -36,12 +37,28 @@ import {registerEmail} from './email'
|
|
|
36
37
|
import {registerGateway} from './gateway'
|
|
37
38
|
import {getGatewayConfig} from './gateway-fetch'
|
|
38
39
|
import {setupModelGateway} from './model-gateway-setup'
|
|
39
|
-
import {setupSession} from './session-setup'
|
|
40
40
|
import {registerOutboundHook, registerOutboundHttpRoute, registerOutboundMethods} from './outbound'
|
|
41
|
+
import {setupSession} from './session-setup'
|
|
41
42
|
import {registerSkillCommandRestore} from './skill-command-restore'
|
|
42
43
|
import {registerTools} from './tools'
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Types aligned with OpenClaw plugin API (openclaw/src/plugins/types.ts)
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
export type PluginLogger = {
|
|
50
|
+
debug?: (message: string) => void
|
|
51
|
+
info: (message: string) => void
|
|
52
|
+
warn: (message: string) => void
|
|
53
|
+
error: (message: string) => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type PluginRuntime = {
|
|
57
|
+
version?: string
|
|
58
|
+
config?: {
|
|
59
|
+
loadConfig?: (...args: unknown[]) => unknown
|
|
60
|
+
writeConfigFile?: (...args: unknown[]) => unknown
|
|
61
|
+
}
|
|
45
62
|
system?: {
|
|
46
63
|
runCommandWithTimeout?: (
|
|
47
64
|
argv: string[],
|
|
@@ -59,31 +76,43 @@ type PluginRuntime = {
|
|
|
59
76
|
}
|
|
60
77
|
}
|
|
61
78
|
|
|
79
|
+
export type GatewayRequestHandler = (opts: {
|
|
80
|
+
params: Record<string, unknown>
|
|
81
|
+
respond: (ok: boolean, payload?: unknown, error?: {code?: string; message?: string}) => void
|
|
82
|
+
}) => Promise<void> | void
|
|
83
|
+
|
|
84
|
+
export type PluginHookName =
|
|
85
|
+
| 'before_agent_start'
|
|
86
|
+
| 'agent_end'
|
|
87
|
+
| 'before_compaction'
|
|
88
|
+
| 'after_compaction'
|
|
89
|
+
| 'before_reset'
|
|
90
|
+
| 'message_received'
|
|
91
|
+
| 'message_sending'
|
|
92
|
+
| 'message_sent'
|
|
93
|
+
| 'before_tool_call'
|
|
94
|
+
| 'after_tool_call'
|
|
95
|
+
| 'tool_result_persist'
|
|
96
|
+
| 'session_start'
|
|
97
|
+
| 'session_end'
|
|
98
|
+
| 'gateway_start'
|
|
99
|
+
| 'gateway_stop'
|
|
100
|
+
|
|
62
101
|
export type PluginApi = {
|
|
102
|
+
/** id from openclaw.plugin.json */
|
|
63
103
|
id: string
|
|
104
|
+
/** name from openclaw.plugin.json */
|
|
64
105
|
name: string
|
|
106
|
+
/** version from openclaw.plugin.json */
|
|
107
|
+
version?: string
|
|
108
|
+
/** description from openclaw.plugin.json */
|
|
109
|
+
description?: string
|
|
110
|
+
source?: string
|
|
111
|
+
config?: Record<string, unknown>
|
|
65
112
|
pluginConfig?: Record<string, unknown>
|
|
66
|
-
logger: {
|
|
67
|
-
info: (msg: string) => void
|
|
68
|
-
warn: (msg: string) => void
|
|
69
|
-
error: (msg: string) => void
|
|
70
|
-
}
|
|
71
113
|
runtime: PluginRuntime
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
handler: (opts: {
|
|
75
|
-
params: Record<string, unknown>
|
|
76
|
-
respond: (ok: boolean, payload?: unknown, error?: {code?: string; message?: string}) => void
|
|
77
|
-
}) => Promise<void> | void,
|
|
78
|
-
) => void
|
|
79
|
-
on: (hookName: string, handler: (...args: any[]) => any, opts?: {priority?: number}) => void
|
|
80
|
-
registerCommand: (cmd: {
|
|
81
|
-
name: string
|
|
82
|
-
description?: string
|
|
83
|
-
acceptsArgs?: boolean
|
|
84
|
-
requireAuth?: boolean
|
|
85
|
-
handler: (ctx: {args?: string}) => Promise<{text: string}> | {text: string}
|
|
86
|
-
}) => void
|
|
114
|
+
logger: PluginLogger
|
|
115
|
+
registerGatewayMethod: (method: string, handler: GatewayRequestHandler) => void
|
|
87
116
|
registerTool: (
|
|
88
117
|
tool: {
|
|
89
118
|
name: string
|
|
@@ -96,7 +125,28 @@ export type PluginApi = {
|
|
|
96
125
|
},
|
|
97
126
|
opts?: {optional?: boolean},
|
|
98
127
|
) => void
|
|
99
|
-
|
|
128
|
+
registerHook?: (
|
|
129
|
+
events: string | string[],
|
|
130
|
+
handler: (...args: unknown[]) => unknown,
|
|
131
|
+
opts?: {name?: string; description?: string; register?: boolean},
|
|
132
|
+
) => void
|
|
133
|
+
registerCommand: (cmd: {
|
|
134
|
+
name: string
|
|
135
|
+
description?: string
|
|
136
|
+
acceptsArgs?: boolean
|
|
137
|
+
requireAuth?: boolean
|
|
138
|
+
handler: (ctx: {
|
|
139
|
+
args?: string
|
|
140
|
+
[key: string]: unknown
|
|
141
|
+
}) => Promise<{text: string}> | {text: string}
|
|
142
|
+
}) => void
|
|
143
|
+
registerChannel: (registration: {plugin: unknown; dock?: unknown}) => void
|
|
144
|
+
registerHttpHandler?: (
|
|
145
|
+
handler: (
|
|
146
|
+
req: import('node:http').IncomingMessage,
|
|
147
|
+
res: import('node:http').ServerResponse,
|
|
148
|
+
) => Promise<boolean> | boolean,
|
|
149
|
+
) => void
|
|
100
150
|
registerHttpRoute: (params: {
|
|
101
151
|
path: string
|
|
102
152
|
handler: (
|
|
@@ -104,6 +154,22 @@ export type PluginApi = {
|
|
|
104
154
|
res: import('node:http').ServerResponse,
|
|
105
155
|
) => Promise<void> | void
|
|
106
156
|
}) => void
|
|
157
|
+
registerCli?: (
|
|
158
|
+
registrar: (...args: unknown[]) => void | Promise<void>,
|
|
159
|
+
opts?: {commands?: string[]},
|
|
160
|
+
) => void
|
|
161
|
+
registerService?: (service: {
|
|
162
|
+
id: string
|
|
163
|
+
start: (...args: unknown[]) => void | Promise<void>
|
|
164
|
+
stop?: (...args: unknown[]) => void | Promise<void>
|
|
165
|
+
}) => void
|
|
166
|
+
registerProvider?: (provider: Record<string, unknown>) => void
|
|
167
|
+
resolvePath?: (input: string) => string
|
|
168
|
+
on: (
|
|
169
|
+
hookName: PluginHookName | (string & {}),
|
|
170
|
+
handler: (...args: any[]) => any,
|
|
171
|
+
opts?: {priority?: number},
|
|
172
|
+
) => void
|
|
107
173
|
}
|
|
108
174
|
|
|
109
175
|
export default {
|
package/lib/stripCliLogs.test.ts
CHANGED
|
@@ -36,9 +36,18 @@ describe('stripCliLogs', () => {
|
|
|
36
36
|
expect(stripCliLogs('')).toBe('')
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
test('
|
|
40
|
-
const input =
|
|
41
|
-
|
|
39
|
+
test('strips config warnings box between logs and JSON', () => {
|
|
40
|
+
const input = [
|
|
41
|
+
'04:49:48 [plugins] feishu_wiki: Registered feishu_wiki tool',
|
|
42
|
+
'│',
|
|
43
|
+
'◇ Config warnings ──────────────────────╮',
|
|
44
|
+
'│ │',
|
|
45
|
+
'│ - plugins.entries.clawly-plugins: ... │',
|
|
46
|
+
'│ │',
|
|
47
|
+
'├─────────────────────────────────────────╯',
|
|
48
|
+
'{"channel":"discord","requests":[]}',
|
|
49
|
+
].join('\n')
|
|
50
|
+
expect(stripCliLogs(input)).toBe('{"channel":"discord","requests":[]}')
|
|
42
51
|
})
|
|
43
52
|
|
|
44
53
|
test('strips ANSI color codes from log lines', () => {
|
package/lib/stripCliLogs.ts
CHANGED
|
@@ -8,18 +8,21 @@
|
|
|
8
8
|
* \x1b[35m[plugins] tool: registered clawly_send_app_push agent tool
|
|
9
9
|
* [{"id":"clawly-plugins", ...}]
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* It may also emit a "Config warnings" box using box-drawing characters
|
|
12
|
+
* (│, ◇, ├, ─, ╮, ╯) between log lines and the JSON body.
|
|
13
|
+
*
|
|
14
|
+
* This function strips ANSI escape sequences, drops every leading line that
|
|
15
|
+
* is not JSON (log prefixes, warning boxes, blank lines), and returns the
|
|
13
16
|
* remaining text (the JSON body).
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
19
|
const ANSI_RE = /\x1b\[[0-9;]*m/g
|
|
17
|
-
const
|
|
20
|
+
const JSON_START_RE = /^\{|^\[[\s\]\[{"]/
|
|
18
21
|
|
|
19
22
|
export function stripCliLogs(output: string): string {
|
|
20
23
|
const cleaned = output.replace(ANSI_RE, '')
|
|
21
24
|
const lines = cleaned.split('\n')
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
return lines.slice(
|
|
25
|
+
const firstJson = lines.findIndex((line) => JSON_START_RE.test(line))
|
|
26
|
+
if (firstJson === -1) return ''
|
|
27
|
+
return lines.slice(firstJson).join('\n').trim()
|
|
25
28
|
}
|