@2en/clawly-plugins 1.22.0 → 1.22.2
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/plugins.ts +9 -10
- package/index.ts +88 -23
- package/lib/stripCliLogs.test.ts +17 -3
- package/lib/stripCliLogs.ts +9 -6
- package/package.json +1 -1
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(
|
package/index.ts
CHANGED
|
@@ -37,12 +37,28 @@ import {registerEmail} from './email'
|
|
|
37
37
|
import {registerGateway} from './gateway'
|
|
38
38
|
import {getGatewayConfig} from './gateway-fetch'
|
|
39
39
|
import {setupModelGateway} from './model-gateway-setup'
|
|
40
|
-
import {setupSession} from './session-setup'
|
|
41
40
|
import {registerOutboundHook, registerOutboundHttpRoute, registerOutboundMethods} from './outbound'
|
|
41
|
+
import {setupSession} from './session-setup'
|
|
42
42
|
import {registerSkillCommandRestore} from './skill-command-restore'
|
|
43
43
|
import {registerTools} from './tools'
|
|
44
44
|
|
|
45
|
-
|
|
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
|
+
}
|
|
46
62
|
system?: {
|
|
47
63
|
runCommandWithTimeout?: (
|
|
48
64
|
argv: string[],
|
|
@@ -60,31 +76,43 @@ type PluginRuntime = {
|
|
|
60
76
|
}
|
|
61
77
|
}
|
|
62
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
|
+
|
|
63
101
|
export type PluginApi = {
|
|
102
|
+
/** id from openclaw.plugin.json */
|
|
64
103
|
id: string
|
|
104
|
+
/** name from openclaw.plugin.json */
|
|
65
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>
|
|
66
112
|
pluginConfig?: Record<string, unknown>
|
|
67
|
-
logger: {
|
|
68
|
-
info: (msg: string) => void
|
|
69
|
-
warn: (msg: string) => void
|
|
70
|
-
error: (msg: string) => void
|
|
71
|
-
}
|
|
72
113
|
runtime: PluginRuntime
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
handler: (opts: {
|
|
76
|
-
params: Record<string, unknown>
|
|
77
|
-
respond: (ok: boolean, payload?: unknown, error?: {code?: string; message?: string}) => void
|
|
78
|
-
}) => Promise<void> | void,
|
|
79
|
-
) => void
|
|
80
|
-
on: (hookName: string, handler: (...args: any[]) => any, opts?: {priority?: number}) => void
|
|
81
|
-
registerCommand: (cmd: {
|
|
82
|
-
name: string
|
|
83
|
-
description?: string
|
|
84
|
-
acceptsArgs?: boolean
|
|
85
|
-
requireAuth?: boolean
|
|
86
|
-
handler: (ctx: {args?: string}) => Promise<{text: string}> | {text: string}
|
|
87
|
-
}) => void
|
|
114
|
+
logger: PluginLogger
|
|
115
|
+
registerGatewayMethod: (method: string, handler: GatewayRequestHandler) => void
|
|
88
116
|
registerTool: (
|
|
89
117
|
tool: {
|
|
90
118
|
name: string
|
|
@@ -97,7 +125,28 @@ export type PluginApi = {
|
|
|
97
125
|
},
|
|
98
126
|
opts?: {optional?: boolean},
|
|
99
127
|
) => void
|
|
100
|
-
|
|
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
|
|
101
150
|
registerHttpRoute: (params: {
|
|
102
151
|
path: string
|
|
103
152
|
handler: (
|
|
@@ -105,6 +154,22 @@ export type PluginApi = {
|
|
|
105
154
|
res: import('node:http').ServerResponse,
|
|
106
155
|
) => Promise<void> | void
|
|
107
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
|
|
108
173
|
}
|
|
109
174
|
|
|
110
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', () => {
|
|
@@ -68,4 +77,9 @@ describe('stripCliLogs', () => {
|
|
|
68
77
|
const input = '[{"id":"test"}]'
|
|
69
78
|
expect(stripCliLogs(input)).toBe('[{"id":"test"}]')
|
|
70
79
|
})
|
|
80
|
+
|
|
81
|
+
test('handles pretty-printed JSON array starting with lone bracket', () => {
|
|
82
|
+
const input = ['11:40:09 [plugins] loading', '[', ' {"id":"test"}', ']'].join('\n')
|
|
83
|
+
expect(stripCliLogs(input)).toBe('[\n {"id":"test"}\n]')
|
|
84
|
+
})
|
|
71
85
|
})
|
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
|
}
|