@2en/clawly-plugins 1.30.0-beta.9 → 1.31.0-beta.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
 
@@ -611,12 +559,14 @@ export function setupConfig(api: PluginApi): void {
611
559
  try {
612
560
  writeOpenclawConfig(configPath, config)
613
561
  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.
562
+ // Refresh the gateway's in-memory runtime config snapshot.
563
+ // The sync write above updates the file on disk, but the gateway
564
+ // caches config via runtimeConfigSnapshot (set during startup by
565
+ // the secrets system). Without this refresh, loadConfig() keeps
566
+ // returning the stale pre-patch config until the next restart.
567
+ void api.runtime.config.writeConfigFile(config).catch((err) => {
568
+ api.logger.warn(`Config setup: runtime snapshot refresh failed: ${(err as Error).message}`)
569
+ })
620
570
  } catch (err) {
621
571
  api.logger.error(`Config setup failed: ${(err as Error).message}`)
622
572
  }
@@ -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.31.0-beta.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)