@2en/clawly-plugins 1.30.0-beta.9 → 1.30.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
@@ -611,12 +611,14 @@ export function setupConfig(api: PluginApi): void {
611
611
  try {
612
612
  writeOpenclawConfig(configPath, config)
613
613
  api.logger.info('Config setup: patched openclaw.json.')
614
- // NOTE: We intentionally do NOT call api.runtime.config.writeConfigFile()
615
- // here. The raw config (from readOpenclawConfig / JSON.parse) lacks
616
- // $include-resolved fields (e.g. models array) and always fails
617
- // validation. The raw disk write above is sufficient — the first
618
- // successful writeConfigFile call from an RPC (like setTimezone)
619
- // will refresh the runtime snapshot.
614
+ // Refresh the gateway's in-memory runtime config snapshot.
615
+ // The sync write above updates the file on disk, but the gateway
616
+ // caches config via runtimeConfigSnapshot (set during startup by
617
+ // the secrets system). Without this refresh, loadConfig() keeps
618
+ // returning the stale pre-patch config until the next restart.
619
+ void api.runtime.config.writeConfigFile(config).catch((err) => {
620
+ api.logger.warn(`Config setup: runtime snapshot refresh failed: ${(err as Error).message}`)
621
+ })
620
622
  } catch (err) {
621
623
  api.logger.error(`Config setup failed: ${(err as Error).message}`)
622
624
  }
@@ -15,7 +15,6 @@
15
15
 
16
16
  import type {PluginApi} from '../types'
17
17
  import type {OpenClawConfig} from '../types/openclaw'
18
- import {backfillDiskConfig} from '../model-gateway-setup'
19
18
 
20
19
  export function registerConfigModel(api: PluginApi) {
21
20
  api.registerGatewayMethod('clawly.config.setModel', async ({params, respond}) => {
@@ -54,13 +53,8 @@ export function registerConfigModel(api: PluginApi) {
54
53
  agents.defaults = defaults
55
54
  config.agents = agents
56
55
 
57
- // Backfill fields written by setupConfig (which writes directly to disk)
58
- // so writeConfigFile's merge-patch doesn't revert them.
59
- const stateDir = api.runtime.state.resolveStateDir()
60
- const configToWrite = stateDir ? backfillDiskConfig(stateDir, config) : config
61
-
62
56
  try {
63
- await api.runtime.config.writeConfigFile(configToWrite)
57
+ await api.runtime.config.writeConfigFile(config)
64
58
  api.logger.info(`config-model: set model.primary to ${model}`)
65
59
  respond(true, {changed: true, model})
66
60
  } catch (err) {
@@ -11,7 +11,6 @@
11
11
 
12
12
  import type {PluginApi} from '../types'
13
13
  import type {OpenClawConfig} from '../types/openclaw'
14
- import {backfillDiskConfig} from '../model-gateway-setup'
15
14
 
16
15
  export function registerConfigTimezone(api: PluginApi) {
17
16
  api.registerGatewayMethod('clawly.config.setTimezone', async ({params, respond}) => {
@@ -47,13 +46,8 @@ export function registerConfigTimezone(api: PluginApi) {
47
46
  agents.defaults = defaults
48
47
  config.agents = agents
49
48
 
50
- // Backfill fields written by setupConfig (which writes directly to disk)
51
- // so writeConfigFile's merge-patch doesn't revert them.
52
- const stateDir = api.runtime.state.resolveStateDir()
53
- const configToWrite = stateDir ? backfillDiskConfig(stateDir, config) : config
54
-
55
49
  try {
56
- await api.runtime.config.writeConfigFile(configToWrite)
50
+ await api.runtime.config.writeConfigFile(config)
57
51
  api.logger.info(`config-timezone: set userTimezone to ${timezone}`)
58
52
  respond(true, {changed: true, timezone})
59
53
  } catch (err) {
@@ -10,7 +10,6 @@
10
10
  */
11
11
 
12
12
  import fs from 'node:fs'
13
- import path from 'node:path'
14
13
 
15
14
  import type {PluginApi} from './index'
16
15
  import type {OpenClawConfig} from './types/openclaw'
@@ -68,48 +67,3 @@ export function patchModelGateway(config: OpenClawConfig, _api: PluginApi): bool
68
67
 
69
68
  return dirty
70
69
  }
71
-
72
- // ---------------------------------------------------------------------------
73
- // backfillDiskConfig — merge setupConfig's disk writes into a loadConfig() result
74
- // ---------------------------------------------------------------------------
75
-
76
- function isPlainObject(v: unknown): v is Record<string, unknown> {
77
- return v !== null && typeof v === 'object' && !Array.isArray(v)
78
- }
79
-
80
- /**
81
- * Deep-merge `source` into `target`, where source wins for any key it has.
82
- * Arrays and primitives from source overwrite target.
83
- * Plain objects recurse.
84
- */
85
- function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): void {
86
- for (const key of Object.keys(source)) {
87
- if (key === '$include') continue // meta-directive, not a resolved config field
88
- const sv = source[key]
89
- const tv = target[key]
90
- if (isPlainObject(sv) && isPlainObject(tv)) {
91
- deepMerge(tv, sv)
92
- } else {
93
- target[key] = sv
94
- }
95
- }
96
- }
97
-
98
- /**
99
- * Read the raw disk config (openclaw.json) and deep-merge its fields into a
100
- * runtime config snapshot (from `loadConfig()`). This ensures that fields
101
- * written by `setupConfig` (which writes directly to disk) are present in the
102
- * config object before it's passed to `writeConfigFile`.
103
- *
104
- * Without this, `writeConfigFile`'s inner merge-patch sees the stale runtime
105
- * snapshot (missing setupConfig's changes) and reverts them on disk.
106
- */
107
- export function backfillDiskConfig(stateDir: string, config: OpenClawConfig): OpenClawConfig {
108
- const configPath = path.join(stateDir, 'openclaw.json')
109
- const diskConfig = readOpenclawConfig(configPath)
110
- // Start from disk (setupConfig's fields) then overlay config (RPC handler's
111
- // changes) so the caller's modifications win over stale disk values.
112
- const merged = {...diskConfig} as Record<string, unknown>
113
- deepMerge(merged, config as Record<string, unknown>)
114
- return merged as OpenClawConfig
115
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2en/clawly-plugins",
3
- "version": "1.30.0-beta.9",
3
+ "version": "1.30.0",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,6 +1,11 @@
1
1
  import {afterAll, beforeEach, describe, expect, test} from 'bun:test'
2
2
  import type {PluginApi} from '../types'
3
- import {registerDeepSearchTool, registerGrokSearchTool, registerSearchTool} from './clawly-search'
3
+ import {
4
+ registerDeepSearchTool,
5
+ registerGrokSearchTool,
6
+ registerKimiSearchTool,
7
+ registerSearchTool,
8
+ } from './clawly-search'
4
9
 
5
10
  // ── Helpers ──────────────────────────────────────────────────────
6
11
 
@@ -267,3 +272,45 @@ describe('clawly_grok_search', () => {
267
272
  expect(res.error).toContain('Grok API error 429')
268
273
  })
269
274
  })
275
+
276
+ // ── clawly_kimi_search ──────────────────────────────────────────
277
+
278
+ describe('clawly_kimi_search', () => {
279
+ test('uses Kimi gateway endpoint with web_search tool', async () => {
280
+ mockResponse.body = {
281
+ choices: [{message: {content: 'Kimi answer with [source](https://example.com/page)'}}],
282
+ }
283
+ const {execute} = createMockApi(registerKimiSearchTool)
284
+ const res = parseResult(await execute('tc-1', {query: 'Chinese news'}))
285
+
286
+ expect(fetchCalls).toHaveLength(1)
287
+ const call = fetchCalls[0]
288
+ expect(call.url).toBe('https://gw.example.com/v1/kimi/v1/chat/completions')
289
+ expect(call.body?.model).toBe('kimi-k2-turbo-preview')
290
+ expect(call.body?.stream).toBe(false)
291
+ expect(call.body?.messages).toEqual([{role: 'user', content: 'Chinese news'}])
292
+ expect(call.body?.tools).toEqual([{type: 'builtin_function', function: {name: 'web_search'}}])
293
+
294
+ expect(res.answer).toBe('Kimi answer with [source](https://example.com/page)')
295
+ expect(res.citations).toEqual(['https://example.com/page'])
296
+ expect(res.provider).toBe('kimi')
297
+ })
298
+
299
+ test('returns empty citations when response has no inline links', async () => {
300
+ mockResponse.body = {choices: [{message: {content: 'Plain answer'}}]}
301
+ const {execute} = createMockApi(registerKimiSearchTool)
302
+ const res = parseResult(await execute('tc-1', {query: 'test'}))
303
+
304
+ expect(res.answer).toBe('Plain answer')
305
+ expect(res.citations).toEqual([])
306
+ expect(res.provider).toBe('kimi')
307
+ })
308
+
309
+ test('returns error on Kimi API failure', async () => {
310
+ mockResponse = {ok: false, status: 429, body: {error: 'rate limited'}}
311
+ const {execute} = createMockApi(registerKimiSearchTool)
312
+ const res = parseResult(await execute('tc-1', {query: 'test'}))
313
+
314
+ expect(res.error).toContain('Kimi API error 429')
315
+ })
316
+ })
@@ -1,9 +1,12 @@
1
1
  import {
2
2
  buildGrokBody,
3
3
  buildGrokUrl,
4
+ buildKimiBody,
5
+ buildKimiUrl,
4
6
  buildPerplexityUrl,
5
7
  createSearchToolRegistrar,
6
8
  parseGrokResponse,
9
+ parseKimiResponse,
7
10
  } from './create-search-tool'
8
11
 
9
12
  export const registerSearchTool = createSearchToolRegistrar({
@@ -40,3 +43,14 @@ export const registerGrokSearchTool = createSearchToolRegistrar({
40
43
  buildBody: buildGrokBody,
41
44
  parseResponse: parseGrokResponse,
42
45
  })
46
+
47
+ export const registerKimiSearchTool = createSearchToolRegistrar({
48
+ toolName: 'clawly_kimi_search',
49
+ description: 'Search the web using Kimi. Good for Chinese-language queries and content.',
50
+ model: 'kimi-k2-turbo-preview',
51
+ buildUrl: buildKimiUrl,
52
+ timeoutMs: 30_000,
53
+ provider: 'kimi',
54
+ buildBody: buildKimiBody,
55
+ parseResponse: parseKimiResponse,
56
+ })
@@ -1,7 +1,7 @@
1
1
  import type {PluginApi} from '../types'
2
2
  import {resolveGatewayCredentials} from '../resolve-gateway-credentials'
3
3
 
4
- export type SearchProvider = 'grok' | 'perplexity'
4
+ export type SearchProvider = 'grok' | 'kimi' | 'perplexity'
5
5
 
6
6
  export interface SearchResult {
7
7
  query: string
@@ -44,6 +44,19 @@ export function buildGrokBody(model: string, query: string): Record<string, unkn
44
44
  return {model, input: query, tools: [{type: 'web_search'}]}
45
45
  }
46
46
 
47
+ export function buildKimiUrl(baseUrl: string): string {
48
+ return `${baseUrl.replace(/\/$/, '')}/kimi/v1/chat/completions`
49
+ }
50
+
51
+ export function buildKimiBody(model: string, query: string): Record<string, unknown> {
52
+ return {
53
+ model,
54
+ stream: false,
55
+ messages: [{role: 'user', content: query}],
56
+ tools: [{type: 'builtin_function', function: {name: 'web_search'}}],
57
+ }
58
+ }
59
+
47
60
  function extractCitations(text: string): string[] {
48
61
  const urls: string[] = []
49
62
  const seen = new Set<string>()
@@ -57,6 +70,12 @@ function extractCitations(text: string): string[] {
57
70
  return urls
58
71
  }
59
72
 
73
+ export function parseKimiResponse(data: unknown): {answer: string; citations: string[]} {
74
+ const d = data as {choices?: {message?: {content?: string}}[]}
75
+ const answerText = d.choices?.[0]?.message?.content ?? ''
76
+ return {answer: answerText, citations: extractCitations(answerText)}
77
+ }
78
+
60
79
  export function parseGrokResponse(data: unknown): {answer: string; citations: string[]} {
61
80
  const typed = data as {
62
81
  output?: Array<{type: string; content?: Array<{type: string; text?: string}>}>
@@ -113,9 +132,13 @@ export function createSearchToolRegistrar(config: SearchToolConfig) {
113
132
 
114
133
  if (!res.ok) {
115
134
  const body = await res.text().catch(() => '')
116
- throw new Error(
117
- `${config.provider === 'grok' ? 'Grok' : 'Perplexity'} API error ${res.status}: ${body.slice(0, 200)}`,
118
- )
135
+ const providerLabel =
136
+ config.provider === 'grok'
137
+ ? 'Grok'
138
+ : config.provider === 'kimi'
139
+ ? 'Kimi'
140
+ : 'Perplexity'
141
+ throw new Error(`${providerLabel} API error ${res.status}: ${body.slice(0, 200)}`)
119
142
  }
120
143
 
121
144
  const data = await res.json()
package/tools/index.ts CHANGED
@@ -2,7 +2,12 @@ import type {PluginApi} from '../types'
2
2
  import {registerCalendarTools} from './clawly-calendar'
3
3
  import {registerIsUserOnlineTool} from './clawly-is-user-online'
4
4
  import {registerMsgBreakTool} from './clawly-msg-break'
5
- import {registerDeepSearchTool, registerGrokSearchTool, registerSearchTool} from './clawly-search'
5
+ import {
6
+ registerDeepSearchTool,
7
+ registerGrokSearchTool,
8
+ registerKimiSearchTool,
9
+ registerSearchTool,
10
+ } from './clawly-search'
6
11
  import {registerSendAppPushTool} from './clawly-send-app-push'
7
12
  import {registerSendFileTool} from './clawly-send-file'
8
13
  import {registerSendMessageTool} from './clawly-send-message'
@@ -14,6 +19,7 @@ export function registerTools(api: PluginApi) {
14
19
  registerSearchTool(api)
15
20
  registerDeepSearchTool(api)
16
21
  registerGrokSearchTool(api)
22
+ registerKimiSearchTool(api)
17
23
  registerSendAppPushTool(api)
18
24
  registerSendFileTool(api)
19
25
  registerSendMessageTool(api)