@2en/clawly-plugins 1.30.0 → 1.31.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/config-setup.ts CHANGED
@@ -24,7 +24,6 @@ import {
24
24
  ENV_KEY_API_KEY,
25
25
  ENV_KEY_BASE,
26
26
  PROVIDER_NAME,
27
- patchModelGateway,
28
27
  readOpenclawConfig,
29
28
  stripPathname,
30
29
  writeOpenclawConfig,
@@ -340,48 +339,82 @@ export function patchSession(config: OpenClawConfig): boolean {
340
339
  return dirty
341
340
  }
342
341
 
343
- const INCLUDE_PATH = './extensions/clawly-plugins/clawly-config-defaults.json5'
342
+ const DEFAULTS_PATH = './extensions/clawly-plugins/clawly-config-defaults.json5'
343
+ const LEGACY_INCLUDE_PATH = DEFAULTS_PATH
344
344
 
345
345
  /**
346
- * Ensures our defaults file is listed in the config's `$include` array.
347
- * Returns true if `$include` was modified.
346
+ * Remove legacy `$include` reference to our defaults file.
347
+ * Returns true if config was modified.
348
348
  */
349
- export function ensureInclude(config: OpenClawConfig & Record<string, unknown>): boolean {
349
+ export function removeInclude(config: OpenClawConfig & Record<string, unknown>): boolean {
350
350
  const existing = config.$include
351
- let includes: string[]
351
+ if (existing === undefined) return false
352
352
 
353
- if (Array.isArray(existing)) {
354
- includes = existing as string[]
355
- } else if (typeof existing === 'string') {
356
- includes = [existing]
357
- } else {
358
- includes = []
353
+ if (typeof existing === 'string') {
354
+ if (existing === LEGACY_INCLUDE_PATH) {
355
+ delete config.$include
356
+ return true
357
+ }
358
+ return false
359
359
  }
360
360
 
361
- if (includes.includes(INCLUDE_PATH)) return false
361
+ if (Array.isArray(existing)) {
362
+ const includes = existing as string[]
363
+ const idx = includes.indexOf(LEGACY_INCLUDE_PATH)
364
+ if (idx === -1) return false
365
+ includes.splice(idx, 1)
366
+ if (includes.length === 0) {
367
+ delete config.$include
368
+ } else if (includes.length === 1) {
369
+ config.$include = includes[0]
370
+ }
371
+ return true
372
+ }
362
373
 
363
- includes.push(INCLUDE_PATH)
364
- config.$include = includes.length === 1 ? includes[0] : includes
365
- return true
374
+ return false
366
375
  }
367
376
 
368
377
  /**
369
- * Restrict the $include defaults file to owner-only (0o600), matching
370
- * openclaw.json. npm/plugin-install creates files with 0o644 by default,
371
- * which would leave config readable by other local users.
378
+ * Deep-merge defaults into config. Config values take precedence;
379
+ * defaults only fill in missing keys. Arrays are not merged config wins.
380
+ * Returns true if any key was added.
372
381
  */
373
- function hardenIncludePermissions(stateDir: string, api: PluginApi): void {
374
- const includePath = path.join(stateDir, INCLUDE_PATH)
375
- try {
376
- const stat = fs.statSync(includePath)
377
- const mode = stat.mode & 0o777
378
- if (mode !== 0o600) {
379
- fs.chmodSync(includePath, 0o600)
380
- api.logger.info(`Hardened ${INCLUDE_PATH} permissions to 0600.`)
382
+ export function deepMergeDefaults(
383
+ config: Record<string, unknown>,
384
+ defaults: Record<string, unknown>,
385
+ ): boolean {
386
+ let merged = false
387
+ for (const key of Object.keys(defaults)) {
388
+ const defaultVal = defaults[key]
389
+ const configVal = config[key]
390
+
391
+ if (configVal === undefined) {
392
+ config[key] = defaultVal
393
+ merged = true
394
+ continue
381
395
  }
382
- } catch {
383
- // File may not exist yet (first install before plugin copies files)
396
+
397
+ // Both are plain objects recurse
398
+ if (
399
+ defaultVal !== null &&
400
+ typeof defaultVal === 'object' &&
401
+ !Array.isArray(defaultVal) &&
402
+ configVal !== null &&
403
+ typeof configVal === 'object' &&
404
+ !Array.isArray(configVal)
405
+ ) {
406
+ if (
407
+ deepMergeDefaults(
408
+ configVal as Record<string, unknown>,
409
+ defaultVal as Record<string, unknown>,
410
+ )
411
+ ) {
412
+ merged = true
413
+ }
414
+ }
415
+ // For arrays and primitives: config wins, do nothing
384
416
  }
417
+ return merged
385
418
  }
386
419
 
387
420
  const PLUGIN_ID = 'clawly-plugins'
@@ -450,89 +483,12 @@ function repairLegacyProvisionState(api: PluginApi, config: OpenClawConfig, stat
450
483
  return installRecordPatched
451
484
  }
452
485
 
453
- /**
454
- * Remove fields from config that are identical to the included defaults.
455
- * Since `$include` deep-merges with main config winning, residual values
456
- * from old JS set-if-missing code "shadow" the json5 defaults and prevent
457
- * future default updates from taking effect.
458
- *
459
- * Only prunes leaf values that exactly match.
460
- * Returns true if any field was deleted.
461
- */
462
- function stableStringify(v: unknown): string {
463
- if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
464
- const obj = v as Record<string, unknown>
465
- return `{${Object.keys(obj)
466
- .sort()
467
- .map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`)
468
- .join(',')}}`
469
- }
470
- return JSON.stringify(v)
471
- }
472
-
473
- export function pruneIncludedDefaults(
474
- config: Record<string, unknown>,
475
- defaults: Record<string, unknown>,
476
- ): boolean {
477
- let pruned = false
478
-
479
- function walk(defaultsNode: Record<string, unknown>, configNode: Record<string, unknown>): void {
480
- for (const key of Object.keys(defaultsNode)) {
481
- const defaultVal = defaultsNode[key]
482
- const configVal = configNode[key]
483
- if (configVal === undefined) continue
484
-
485
- // Both are plain objects → recurse
486
- if (
487
- defaultVal !== null &&
488
- typeof defaultVal === 'object' &&
489
- !Array.isArray(defaultVal) &&
490
- configVal !== null &&
491
- typeof configVal === 'object' &&
492
- !Array.isArray(configVal)
493
- ) {
494
- walk(defaultVal as Record<string, unknown>, configVal as Record<string, unknown>)
495
- // Clean up empty parent objects left behind after pruning
496
- if (Object.keys(configVal as Record<string, unknown>).length === 0) {
497
- delete configNode[key]
498
- pruned = true
499
- }
500
- continue
501
- }
502
-
503
- // Both are non-empty arrays → remove config elements that exist in defaults
504
- if (Array.isArray(defaultVal) && defaultVal.length > 0 && Array.isArray(configVal)) {
505
- const defaultSet = new Set(defaultVal.map((v) => stableStringify(v)))
506
- const filtered = configVal.filter((v) => !defaultSet.has(stableStringify(v)))
507
- if (filtered.length !== configVal.length) {
508
- if (filtered.length === 0) {
509
- delete configNode[key]
510
- } else {
511
- configNode[key] = filtered
512
- }
513
- pruned = true
514
- }
515
- continue
516
- }
517
-
518
- // Leaf comparison: use stableStringify for key-order-insensitive deep equality
519
- if (stableStringify(configVal) === stableStringify(defaultVal)) {
520
- delete configNode[key]
521
- pruned = true
522
- }
523
- }
524
- }
525
-
526
- walk(defaults, config)
527
- return pruned
528
- }
529
-
530
486
  /**
531
487
  * Load and parse clawly-config-defaults.json5 from the plugin install directory.
532
488
  * Uses the bundled `json5` package (declared in package.json).
533
489
  */
534
- function loadIncludedDefaults(stateDir: string): Record<string, unknown> | null {
535
- const defaultsPath = path.join(stateDir, INCLUDE_PATH)
490
+ function loadDefaults(stateDir: string): Record<string, unknown> | null {
491
+ const defaultsPath = path.join(stateDir, DEFAULTS_PATH)
536
492
  try {
537
493
  const raw = fs.readFileSync(defaultsPath, 'utf-8')
538
494
  return JSON5.parse(raw) as Record<string, unknown>
@@ -565,13 +521,12 @@ function reconcileRuntimeConfig(
565
521
  dirty = patchGateway(config) || dirty
566
522
  dirty = patchBrowser(config) || dirty
567
523
  dirty = patchSession(config) || dirty
568
- dirty = patchModelGateway(config, api) || dirty
569
524
 
570
- const defaults = loadIncludedDefaults(stateDir)
525
+ const defaults = loadDefaults(stateDir)
571
526
  if (defaults) {
572
- dirty = pruneIncludedDefaults(config, defaults) || dirty
527
+ dirty = deepMergeDefaults(config, defaults) || dirty
573
528
  } else {
574
- api.logger.warn('Config setup: failed to load included defaults, skipping dedup.')
529
+ api.logger.warn('Config setup: failed to load defaults json5, skipping merge.')
575
530
  }
576
531
 
577
532
  return dirty
@@ -588,22 +543,15 @@ export function setupConfig(api: PluginApi): void {
588
543
  return
589
544
  }
590
545
 
591
- // OpenClaw exposes three config layers:
592
- // parsed — raw JSON from openclaw.json (no $include, no env expansion)
593
- // resolved parsed + $include deep-merge + ${ENV} interpolation
594
- // runtime — resolved + SecretRef resolution, cached in-memory snapshot
595
- //
596
- // We need parsed: patchers mutate raw fields and write back via
597
- // writeConfigFile, which diffs against the runtime snapshot and projects
598
- // changes onto the source file. Using resolved/runtime would inline
599
- // $include defaults into openclaw.json, defeating the json5 migration.
546
+ // Read the raw parsed config from openclaw.json (no env expansion).
547
+ // Patchers mutate fields in place; defaults from json5 are deep-merged
548
+ // to fill missing keys. The result is written back as the full config.
600
549
  const configPath = path.join(stateDir, 'openclaw.json')
601
550
  const config = readOpenclawConfig(configPath)
602
551
  const pc = toPCMerged(api, config)
603
552
 
604
553
  let dirty = false
605
- dirty = ensureInclude(config) || dirty
606
- hardenIncludePermissions(stateDir, api)
554
+ dirty = removeInclude(config) || dirty
607
555
  dirty = repairLegacyProvisionState(api, config, stateDir) || dirty
608
556
  dirty = reconcileRuntimeConfig(api, config, pc, stateDir) || dirty
609
557
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2en/clawly-plugins",
3
- "version": "1.30.0",
3
+ "version": "1.31.0",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "repository": {
@@ -289,7 +289,7 @@ describe('clawly_kimi_search', () => {
289
289
  expect(call.body?.model).toBe('kimi-k2-turbo-preview')
290
290
  expect(call.body?.stream).toBe(false)
291
291
  expect(call.body?.messages).toEqual([{role: 'user', content: 'Chinese news'}])
292
- expect(call.body?.tools).toEqual([{type: 'builtin_function', function: {name: 'web_search'}}])
292
+ expect(call.body?.tools).toEqual([{type: 'builtin_function', function: {name: '$web_search'}}])
293
293
 
294
294
  expect(res.answer).toBe('Kimi answer with [source](https://example.com/page)')
295
295
  expect(res.citations).toEqual(['https://example.com/page'])
@@ -53,7 +53,7 @@ export function buildKimiBody(model: string, query: string): Record<string, unkn
53
53
  model,
54
54
  stream: false,
55
55
  messages: [{role: 'user', content: query}],
56
- tools: [{type: 'builtin_function', function: {name: 'web_search'}}],
56
+ tools: [{type: 'builtin_function', function: {name: '$web_search'}}],
57
57
  }
58
58
  }
59
59