@2en/clawly-plugins 1.19.2 → 1.20.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.
@@ -17,6 +17,8 @@ import {stripCliLogs} from '../lib/stripCliLogs'
17
17
 
18
18
  $.verbose = false
19
19
 
20
+ const PLUGIN_SENTINEL = 'openclaw.plugin.json'
21
+
20
22
  interface NpmViewCache {
21
23
  version: string
22
24
  versions: string[]
@@ -56,6 +58,74 @@ async function fetchNpmView(npmPkgName: string): Promise<NpmViewCache | null> {
56
58
  }
57
59
  }
58
60
 
61
+ /**
62
+ * Back up plugin dir before an install/update operation, verify the sentinel
63
+ * file exists after, and rollback from backup on failure. Prevents the
64
+ * "deleted plugin dir with no recovery" scenario seen in failed auto-updates.
65
+ */
66
+ async function withPluginBackup<T>(
67
+ api: PluginApi,
68
+ pluginId: string,
69
+ operation: () => Promise<T>,
70
+ ): Promise<T> {
71
+ const stateDir = resolveStateDir(api)
72
+ if (!stateDir) return operation()
73
+
74
+ const extensionDir = path.join(stateDir, 'extensions', pluginId)
75
+ const backupDir = `${extensionDir}.update-backup`
76
+
77
+ // Back up only if there's a valid existing installation.
78
+ // If backup fails, abort — a working plugin is more valuable than a risky update.
79
+ const hasExisting = fs.existsSync(path.join(extensionDir, PLUGIN_SENTINEL))
80
+ if (hasExisting) {
81
+ try {
82
+ if (fs.existsSync(backupDir)) await $`rm -rf ${backupDir}`
83
+ await $`cp -a ${extensionDir} ${backupDir}`
84
+ api.logger.info(`plugins: backed up ${pluginId}`)
85
+ } catch (err) {
86
+ // Clean up partial backup before aborting
87
+ if (fs.existsSync(backupDir)) await $`rm -rf ${backupDir}`.catch(() => {})
88
+ throw new Error(
89
+ `plugin backup failed, aborting update: ${err instanceof Error ? err.message : String(err)}`,
90
+ )
91
+ }
92
+ }
93
+
94
+ try {
95
+ const result = await operation()
96
+
97
+ // Verify sentinel exists after operation
98
+ if (!fs.existsSync(path.join(extensionDir, PLUGIN_SENTINEL))) {
99
+ // Clean up partial install if no backup exists (first-time install failure)
100
+ if (!hasExisting && fs.existsSync(extensionDir)) {
101
+ await $`rm -rf ${extensionDir}`.catch(() => {})
102
+ }
103
+ throw new Error(`plugin files missing after operation (${PLUGIN_SENTINEL} not found)`)
104
+ }
105
+
106
+ // Clean backup on success
107
+ if (fs.existsSync(backupDir)) {
108
+ await $`rm -rf ${backupDir}`.catch(() => {})
109
+ }
110
+
111
+ return result
112
+ } catch (err) {
113
+ if (fs.existsSync(backupDir)) {
114
+ api.logger.warn(`plugins: rolling back ${pluginId} from backup`)
115
+ try {
116
+ if (fs.existsSync(extensionDir)) await $`rm -rf ${extensionDir}`
117
+ await $`mv ${backupDir} ${extensionDir}`
118
+ api.logger.info(`plugins: rollback complete`)
119
+ } catch (rollbackErr) {
120
+ api.logger.error(
121
+ `plugins: rollback failed — ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
122
+ )
123
+ }
124
+ }
125
+ throw err
126
+ }
127
+ }
128
+
59
129
  export function registerPlugins(api: PluginApi) {
60
130
  // ── clawly.plugins.version ──────────────────────────────────────
61
131
 
@@ -179,41 +249,48 @@ export function registerPlugins(api: PluginApi) {
179
249
  // ignore
180
250
  }
181
251
 
182
- // 3. Remove extension dir to force clean install
183
- const extensionDir = path.join(stateDir, 'extensions', pluginId)
184
- if (fs.existsSync(extensionDir)) {
185
- api.logger.info(`plugins: force — removing ${extensionDir}`)
186
- await $`rm -rf ${extensionDir}`
187
- }
188
-
189
- // 4. Install fresh
190
- api.logger.info(`plugins: force — installing ${installTarget}`)
191
- const result = await $`openclaw plugins install ${installTarget}`
192
- output = result.stdout.trim()
193
-
194
- // 5. Merge saved config back into plugins.entries.<id>
252
+ // 3-4. Back up existing dir, remove, install rollback on failure
195
253
  try {
196
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
197
- if (!config.plugins) config.plugins = {}
198
- if (!config.plugins.entries) config.plugins.entries = {}
199
- config.plugins.entries[pluginId] = {
200
- ...config.plugins.entries[pluginId],
201
- ...savedEntry,
254
+ output = await withPluginBackup(api, pluginId, async () => {
255
+ const extensionDir = path.join(stateDir, 'extensions', pluginId)
256
+ if (fs.existsSync(extensionDir)) {
257
+ api.logger.info(`plugins: force — removing ${extensionDir}`)
258
+ await $`rm -rf ${extensionDir}`
259
+ }
260
+
261
+ api.logger.info(`plugins: force — installing ${installTarget}`)
262
+ const result = await $`openclaw plugins install ${installTarget}`
263
+ return result.stdout.trim()
264
+ })
265
+ } finally {
266
+ // 5. Always restore config entry (both on success and after rollback)
267
+ try {
268
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
269
+ if (!config.plugins) config.plugins = {}
270
+ if (!config.plugins.entries) config.plugins.entries = {}
271
+ config.plugins.entries[pluginId] = {
272
+ ...config.plugins.entries[pluginId],
273
+ ...savedEntry,
274
+ }
275
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n')
276
+ api.logger.info(`plugins: force — restored entry config for ${pluginId}`)
277
+ } catch (err) {
278
+ api.logger.warn(
279
+ `plugins: force — failed to restore entry config: ${err instanceof Error ? err.message : String(err)}`,
280
+ )
202
281
  }
203
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n')
204
- api.logger.info(`plugins: force — restored entry config for ${pluginId}`)
205
- } catch (err) {
206
- api.logger.warn(
207
- `plugins: force — failed to restore entry config: ${err instanceof Error ? err.message : String(err)}`,
208
- )
209
282
  }
210
283
  } else if (strategy === 'update') {
211
- const result = await $`openclaw plugins update ${pluginId}`
212
- output = result.stdout.trim()
284
+ output = await withPluginBackup(api, pluginId, async () => {
285
+ const result = await $`openclaw plugins update ${pluginId}`
286
+ return result.stdout.trim()
287
+ })
213
288
  } else {
214
289
  // install
215
- const result = await $`openclaw plugins install ${installTarget}`
216
- output = result.stdout.trim()
290
+ output = await withPluginBackup(api, pluginId, async () => {
291
+ const result = await $`openclaw plugins install ${installTarget}`
292
+ return result.stdout.trim()
293
+ })
217
294
  }
218
295
 
219
296
  // Invalidate npm cache for this package
@@ -23,6 +23,12 @@ export const EXTRA_GATEWAY_MODELS: Array<{
23
23
  alias: string
24
24
  input: string[]
25
25
  }> = [
26
+ {
27
+ id: 'moonshotai/kimi-k2.5',
28
+ name: 'moonshotai/kimi-k2.5',
29
+ alias: 'Kimi K2.5',
30
+ input: ['text', 'image'],
31
+ },
26
32
  {
27
33
  id: 'anthropic/claude-sonnet-4.6',
28
34
  name: 'anthropic/claude-sonnet-4.6',
@@ -104,15 +110,38 @@ export function setupModelGateway(api: PluginApi): void {
104
110
  }
105
111
  }
106
112
 
107
- // Ensure aliases exist for all models
113
+ // Ensure aliases exist for ALL models (existing + extras)
108
114
  const agents = (config.agents ?? {}) as any
109
115
  const defaults = agents.defaults ?? {}
110
116
  const existingAliases: Record<string, {alias: string}> = defaults.models ?? {}
111
- for (const m of EXTRA_GATEWAY_MODELS) {
117
+ for (const m of existingModels) {
112
118
  const key = `${PROVIDER_NAME}/${m.id}`
113
119
  if (!existingAliases[key]) {
114
- existingAliases[key] = {alias: m.alias}
120
+ const extra = EXTRA_GATEWAY_MODELS.find((e) => e.id === m.id)
121
+ existingAliases[key] = {alias: extra?.alias ?? m.id}
122
+ dirty = true
123
+ }
124
+ }
125
+
126
+ // Backfill pluginConfig from existing provider credentials (legacy sprites
127
+ // provisioned before plugin config was written during configure phase).
128
+ const cfg = api.pluginConfig as Record<string, unknown> | undefined
129
+ if (
130
+ existingProvider.baseUrl &&
131
+ existingProvider.apiKey &&
132
+ (!cfg?.modelGatewayBaseUrl || !cfg?.modelGatewayToken)
133
+ ) {
134
+ const entries = (config.plugins as any)?.entries?.['clawly-plugins']
135
+ if (entries) {
136
+ entries.config = {
137
+ ...entries.config,
138
+ modelGatewayBaseUrl: existingProvider.baseUrl,
139
+ modelGatewayToken: existingProvider.apiKey,
140
+ }
115
141
  dirty = true
142
+ api.logger.info(
143
+ 'Model gateway: backfilled pluginConfig from existing provider credentials.',
144
+ )
116
145
  }
117
146
  }
118
147
 
@@ -169,9 +198,14 @@ export function setupModelGateway(api: PluginApi): void {
169
198
  {id: imageModel, name: imageModel, input: ['text', 'image']},
170
199
  ]
171
200
 
201
+ const defaultIds = new Set(defaultModels.map((m) => m.id))
172
202
  const models = [
173
203
  ...defaultModels,
174
- ...EXTRA_GATEWAY_MODELS.map(({id, name, input}) => ({id, name, input})),
204
+ ...EXTRA_GATEWAY_MODELS.filter((m) => !defaultIds.has(m.id)).map(({id, name, input}) => ({
205
+ id,
206
+ name,
207
+ input,
208
+ })),
175
209
  ]
176
210
 
177
211
  if (!config.models) config.models = {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2en/clawly-plugins",
3
- "version": "1.19.2",
3
+ "version": "1.20.1",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "repository": {