@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 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
  }
@@ -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 backupDir = `${extensionDir}.update-backup`
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(backupDir)) await $`rm -rf ${backupDir}`
83
- await $`cp -a ${extensionDir} ${backupDir}`
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
- // Clean up partial backup before aborting
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(backupDir)) {
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(backupDir)) {
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 $`mv ${backupDir} ${extensionDir}`
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
- type PluginRuntime = {
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
- registerGatewayMethod: (
73
- method: string,
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
- registerChannel: (registration: {plugin: any}) => void
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 {
@@ -36,9 +36,18 @@ describe('stripCliLogs', () => {
36
36
  expect(stripCliLogs('')).toBe('')
37
37
  })
38
38
 
39
- test('does not strip lines that merely contain a timestamp mid-line', () => {
40
- const input = 'some prefix 11:40:09 [x] y\n{"ok":true}'
41
- expect(stripCliLogs(input)).toBe('some prefix 11:40:09 [x] y\n{"ok":true}')
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', () => {
@@ -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
- * This function strips ANSI escape sequences, drops every line that looks
12
- * like a log prefix (`HH:MM:SS [tag] ...` or `[tag] ...`), and returns the
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 LOG_LINE_RE = /^(\d{1,2}:\d{2}:\d{2} )?\[\w[\w-]*\] /
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 firstNonLog = lines.findIndex((line) => !LOG_LINE_RE.test(line))
23
- if (firstNonLog === -1) return ''
24
- return lines.slice(firstNonLog).join('\n').trim()
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2en/clawly-plugins",
3
- "version": "1.21.6",
3
+ "version": "1.22.1",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "repository": {