@anionex/dsh-tool-search 0.1.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +160 -0
  4. package/README.zh.md +160 -0
  5. package/cordis.patch.yml +3 -0
  6. package/docs/benchmark-results.json +51 -0
  7. package/lib/bm25.js +83 -0
  8. package/lib/bm25.js.map +1 -0
  9. package/lib/catalog.js +53 -0
  10. package/lib/catalog.js.map +1 -0
  11. package/lib/client.js +388 -0
  12. package/lib/client.js.map +1 -0
  13. package/lib/index.js +24 -0
  14. package/lib/index.js.map +1 -0
  15. package/lib/runtime.js +286 -0
  16. package/lib/runtime.js.map +1 -0
  17. package/lib/settings.js +50 -0
  18. package/lib/settings.js.map +1 -0
  19. package/lib/shared.js +40 -0
  20. package/lib/shared.js.map +1 -0
  21. package/lib/types/bm25.d.ts +20 -0
  22. package/lib/types/bm25.d.ts.map +1 -0
  23. package/lib/types/catalog.d.ts +26 -0
  24. package/lib/types/catalog.d.ts.map +1 -0
  25. package/lib/types/client/index.d.ts +65 -0
  26. package/lib/types/client/index.d.ts.map +1 -0
  27. package/lib/types/index.d.ts +14 -0
  28. package/lib/types/index.d.ts.map +1 -0
  29. package/lib/types/runtime.d.ts +37 -0
  30. package/lib/types/runtime.d.ts.map +1 -0
  31. package/lib/types/settings.d.ts +16 -0
  32. package/lib/types/settings.d.ts.map +1 -0
  33. package/lib/types/shared.d.ts +31 -0
  34. package/lib/types/shared.d.ts.map +1 -0
  35. package/lib/types/web.d.ts +5 -0
  36. package/lib/types/web.d.ts.map +1 -0
  37. package/lib/web.js +28 -0
  38. package/lib/web.js.map +1 -0
  39. package/package.json +149 -0
  40. package/pnpm-workspace.yaml +11 -0
  41. package/scripts/benchmark.mjs +124 -0
  42. package/scripts/profile-e2e.mjs +366 -0
  43. package/scripts/validate.mjs +170 -0
  44. package/src/bm25.ts +104 -0
  45. package/src/catalog.ts +72 -0
  46. package/src/client/index.tsx +344 -0
  47. package/src/index.ts +32 -0
  48. package/src/runtime.ts +326 -0
  49. package/src/settings.ts +63 -0
  50. package/src/shared.ts +63 -0
  51. package/src/web.ts +34 -0
  52. package/src/wink-porter2-stemmer.d.ts +3 -0
  53. package/tests/bm25.spec.ts +75 -0
  54. package/tests/catalog.spec.ts +63 -0
  55. package/tests/client.spec.tsx +39 -0
  56. package/tests/execution-regression.spec.ts +200 -0
  57. package/tests/runtime.spec.ts +271 -0
  58. package/tests/settings.spec.ts +33 -0
  59. package/tsconfig.client.json +31 -0
  60. package/tsconfig.json +30 -0
  61. package/tsconfig.test.json +23 -0
  62. package/tsdown.config.mjs +37 -0
package/src/catalog.ts ADDED
@@ -0,0 +1,72 @@
1
+ /** Codex-compatible tool search document construction. */
2
+
3
+ export interface JsonSchemaLike {
4
+ description?: unknown
5
+ properties?: unknown
6
+ items?: unknown
7
+ anyOf?: unknown
8
+ }
9
+
10
+ export interface ToolSchemaLike {
11
+ name: string
12
+ description?: string
13
+ parameters: unknown
14
+ }
15
+
16
+ export interface ToolCatalogEntry {
17
+ name: string
18
+ description: string
19
+ parameters: unknown
20
+ searchText: string
21
+ }
22
+
23
+ function pushPart(parts: string[], value: unknown): void {
24
+ if (typeof value !== 'string') return
25
+ const part = value.trim()
26
+ if (part.length > 0) parts.push(part)
27
+ }
28
+
29
+ /**
30
+ * Append descriptions and property names in the same recursive shape used by
31
+ * Codex: description, sorted object properties, array items, then anyOf.
32
+ */
33
+ export function appendSchemaSearchText(schema: unknown, parts: string[]): void {
34
+ if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) return
35
+ const node = schema as JsonSchemaLike
36
+ pushPart(parts, node.description)
37
+
38
+ if (typeof node.properties === 'object' && node.properties !== null && !Array.isArray(node.properties)) {
39
+ const properties = node.properties as Record<string, unknown>
40
+ for (const name of Object.keys(properties).sort()) {
41
+ pushPart(parts, name)
42
+ appendSchemaSearchText(properties[name], parts)
43
+ }
44
+ }
45
+ appendSchemaSearchText(node.items, parts)
46
+ if (Array.isArray(node.anyOf)) {
47
+ for (const variant of node.anyOf) appendSchemaSearchText(variant, parts)
48
+ }
49
+ }
50
+
51
+ export function toolSearchText(tool: ToolSchemaLike): string {
52
+ const parts: string[] = []
53
+ pushPart(parts, tool.name)
54
+ pushPart(parts, tool.name.replaceAll('_', ' '))
55
+ pushPart(parts, tool.description)
56
+ appendSchemaSearchText(tool.parameters, parts)
57
+ return parts.join(' ')
58
+ }
59
+
60
+ export function buildCatalog(tools: readonly ToolSchemaLike[]): ToolCatalogEntry[] {
61
+ const byName = new Map<string, ToolCatalogEntry>()
62
+ for (const tool of tools) {
63
+ if (byName.has(tool.name)) continue
64
+ byName.set(tool.name, {
65
+ name: tool.name,
66
+ description: tool.description?.trim() ?? '',
67
+ parameters: structuredClone(tool.parameters),
68
+ searchText: toolSearchText(tool),
69
+ })
70
+ }
71
+ return [...byName.values()].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)
72
+ }
@@ -0,0 +1,344 @@
1
+ /** DSH Web settings surface for deferred tool policy. */
2
+
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useState,
8
+ useSyncExternalStore,
9
+ type ReactNode,
10
+ } from 'react'
11
+ import {
12
+ DEFAULT_CORE_TOOLS,
13
+ DEFAULT_RESULT_LIMIT,
14
+ MAX_RESULT_LIMIT,
15
+ TOOL_SEARCH_CATALOG_ROUTE,
16
+ TOOL_SEARCH_NAME,
17
+ TOOL_SEARCH_SETTINGS_NAMESPACE,
18
+ type CatalogSnapshot,
19
+ type ToolPolicy,
20
+ type ToolSearchSettings,
21
+ } from '../shared.ts'
22
+
23
+ interface SettingsScopeSnapshot<T> {
24
+ status: 'loading' | 'ready' | 'unavailable'
25
+ value: T | undefined
26
+ writable: boolean
27
+ }
28
+
29
+ interface SettingsScope<T> {
30
+ getSnapshot(): SettingsScopeSnapshot<T>
31
+ subscribe(listener: () => void): () => void
32
+ set(field: string, value: unknown): Promise<void>
33
+ }
34
+
35
+ type Disposer = () => void
36
+
37
+ interface ClientContext {
38
+ effect(setup: () => Disposer | void, label: string): void
39
+ locale: {
40
+ register(namespace: string, catalogs: Record<string, Record<string, string>>): Disposer
41
+ bind(namespace: string): (key: string) => string
42
+ }
43
+ settingsScope: {
44
+ bind<T>(options: { namespace: string }): SettingsScope<T>
45
+ }
46
+ slots: {
47
+ inject(name: string, callback: () => Disposer): Disposer
48
+ register<P>(
49
+ options: {
50
+ name: string
51
+ id: string
52
+ order: number
53
+ label: () => string
54
+ inject: () => P
55
+ },
56
+ component: (props: P) => ReactNode,
57
+ ): Disposer
58
+ }
59
+ }
60
+
61
+ type Translate = (key: keyof typeof en) => string
62
+
63
+ const en = {
64
+ nav: 'Tool Search',
65
+ topK: 'Default results',
66
+ alwaysVisible: 'Always visible',
67
+ neverSearch: 'Never searchable',
68
+ catalog: 'Tool catalog',
69
+ filter: 'Filter tools',
70
+ refresh: 'Refresh',
71
+ tool: 'Tool',
72
+ policy: 'Policy',
73
+ selected: 'Live agents',
74
+ defaultPolicy: 'Default',
75
+ alwaysPolicy: 'Always',
76
+ blockedPolicy: 'Blocked',
77
+ loading: 'Loading...',
78
+ empty: 'No matching tools',
79
+ saved: 'Saved',
80
+ onePerLine: 'One exact tool name per line',
81
+ }
82
+
83
+ const zh: typeof en = {
84
+ nav: '工具搜索',
85
+ topK: '默认结果数',
86
+ alwaysVisible: '常驻白名单',
87
+ neverSearch: '搜索黑名单',
88
+ catalog: '工具目录',
89
+ filter: '筛选工具',
90
+ refresh: '刷新',
91
+ tool: '工具',
92
+ policy: '策略',
93
+ selected: '活跃 Agent',
94
+ defaultPolicy: '默认',
95
+ alwaysPolicy: '常驻',
96
+ blockedPolicy: '禁用',
97
+ loading: '加载中...',
98
+ empty: '没有匹配工具',
99
+ saved: '已保存',
100
+ onePerLine: '每行一个精确工具名',
101
+ }
102
+
103
+ const EMPTY_SETTINGS: SettingsScopeSnapshot<ToolSearchSettings> = {
104
+ status: 'unavailable',
105
+ value: undefined,
106
+ writable: false,
107
+ }
108
+
109
+ const styles = `
110
+ .dts-settings{container-type:inline-size;display:flex;flex-direction:column;gap:20px;width:100%;min-width:0;color:var(--dsw-alias-label-primary)}
111
+ .dts-toolbar{display:grid;grid-template-columns:minmax(160px,220px) minmax(220px,1fr) auto;align-items:end;gap:12px}
112
+ .dts-field{display:flex;flex-direction:column;gap:6px;min-width:0}
113
+ .dts-field>span,.dts-section-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}
114
+ .dts-input,.dts-textarea,.dts-select{box-sizing:border-box;width:100%;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit}
115
+ .dts-input,.dts-select{height:34px;padding:0 9px}.dts-textarea{min-height:104px;padding:8px 9px;resize:vertical;line-height:20px}
116
+ .dts-input:focus,.dts-textarea:focus,.dts-select:focus{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
117
+ .dts-lists{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
118
+ .dts-hint{font-size:11px;color:var(--dsw-alias-label-tertiary)}
119
+ .dts-button{height:34px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);cursor:pointer}
120
+ .dts-button:hover{background:var(--dsw-alias-interactive-bg-hover)}.dts-button:disabled{opacity:.5;cursor:not-allowed}
121
+ .dts-status{min-height:18px;font-size:12px;color:var(--dsw-alias-label-secondary)}.dts-status[data-error=true]{color:var(--dsw-alias-state-error-primary)}
122
+ .dts-table-wrap{min-width:0;overflow:auto;border-top:1px solid var(--dsw-alias-border-l2);border-bottom:1px solid var(--dsw-alias-border-l2)}
123
+ .dts-table{width:100%;min-width:620px;border-collapse:collapse;table-layout:fixed}
124
+ .dts-table th,.dts-table td{box-sizing:border-box;padding:9px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);text-align:left;vertical-align:middle;font-size:12px}
125
+ .dts-table th{position:sticky;top:0;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-weight:600}
126
+ .dts-table tr:last-child td{border-bottom:0}.dts-tool{width:52%}.dts-tool code{display:block;overflow:hidden;text-overflow:ellipsis;overflow-wrap:anywhere;white-space:nowrap;color:var(--dsw-alias-label-primary)}
127
+ .dts-description{display:block;margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary)}
128
+ .dts-policy{width:26%}.dts-selected{width:22%;color:var(--dsw-alias-label-secondary)}
129
+ @container(max-width:620px){.dts-toolbar{grid-template-columns:1fr auto}.dts-toolbar .dts-limit{grid-column:1/-1}.dts-lists{grid-template-columns:1fr}.dts-table{min-width:0}.dts-table thead{display:none}.dts-table tr{display:grid;grid-template-columns:minmax(0,1fr) 132px;padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dts-table td{padding:4px 0;border:0}.dts-table .dts-tool,.dts-table .dts-policy{width:auto}.dts-table .dts-selected{grid-column:1/-1;width:auto;font-size:11px}.dts-table .dts-selected::before{content:attr(data-label) ": ";color:var(--dsw-alias-label-tertiary);font-weight:600}}
130
+ `
131
+
132
+ function namesFromText(value: string): string[] {
133
+ return [...new Set(value.split(/\r?\n/u).map(name => name.trim()).filter(Boolean))].sort()
134
+ }
135
+
136
+ function overrideFor(name: string, settings: ToolSearchSettings): ToolPolicy | 'default' {
137
+ if (settings.neverSearch.includes(name)) return 'blocked'
138
+ if (settings.alwaysVisible.includes(name)) return 'always'
139
+ return 'default'
140
+ }
141
+
142
+ function decodeCatalog(value: unknown): CatalogSnapshot {
143
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error('Invalid catalog response')
144
+ const record = value as Record<string, unknown>
145
+ if (record['schemaVersion'] !== 1 || !Array.isArray(record['tools']) || typeof record['generation'] !== 'number') {
146
+ throw new Error('Invalid catalog response')
147
+ }
148
+ return value as CatalogSnapshot
149
+ }
150
+
151
+ type SettingsProps = {
152
+ scope?: SettingsScope<ToolSearchSettings>
153
+ t?: Translate
154
+ }
155
+
156
+ export function ToolSearchSettingsSection({ scope, t }: SettingsProps): ReactNode {
157
+ const snapshot = useSyncExternalStore(
158
+ useCallback((notify: () => void) => scope?.subscribe(notify) ?? (() => {}), [scope]),
159
+ useCallback(() => scope?.getSnapshot() ?? EMPTY_SETTINGS, [scope]),
160
+ )
161
+ const [catalog, setCatalog] = useState<CatalogSnapshot>()
162
+ const [filter, setFilter] = useState('')
163
+ const [alwaysText, setAlwaysText] = useState('')
164
+ const [blockedText, setBlockedText] = useState('')
165
+ const [limitText, setLimitText] = useState(String(DEFAULT_RESULT_LIMIT))
166
+ const [status, setStatus] = useState<{ text: string; error: boolean }>()
167
+ const translate = t ?? ((key: keyof typeof en) => en[key])
168
+ const settings = snapshot.value
169
+
170
+ const refresh = useCallback(async (): Promise<void> => {
171
+ try {
172
+ const response = await fetch(TOOL_SEARCH_CATALOG_ROUTE, { cache: 'no-store' })
173
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
174
+ setCatalog(decodeCatalog(await response.json()))
175
+ setStatus(undefined)
176
+ } catch (error) {
177
+ setStatus({ text: error instanceof Error ? error.message : String(error), error: true })
178
+ }
179
+ }, [])
180
+
181
+ useEffect(() => { void refresh() }, [refresh])
182
+ useEffect(() => {
183
+ if (settings === undefined) return
184
+ setAlwaysText(settings.alwaysVisible.join('\n'))
185
+ setBlockedText(settings.neverSearch.join('\n'))
186
+ setLimitText(String(settings.defaultLimit))
187
+ }, [settings])
188
+
189
+ const commit = useCallback((field: keyof ToolSearchSettings, value: unknown): void => {
190
+ if (scope === undefined || !snapshot.writable) return
191
+ setStatus(undefined)
192
+ void scope.set(field, value).then(() => {
193
+ setStatus({ text: translate('saved'), error: false })
194
+ void refresh()
195
+ }, error => {
196
+ setStatus({ text: error instanceof Error ? error.message : String(error), error: true })
197
+ })
198
+ }, [refresh, scope, snapshot.writable, translate])
199
+
200
+ const visibleTools = useMemo(() => {
201
+ const query = filter.trim().toLowerCase()
202
+ if (query.length === 0) return catalog?.tools ?? []
203
+ return (catalog?.tools ?? []).filter(tool =>
204
+ tool.name.toLowerCase().includes(query) || tool.description.toLowerCase().includes(query),
205
+ )
206
+ }, [catalog, filter])
207
+
208
+ const changePolicy = (name: string, next: ToolPolicy | 'default'): void => {
209
+ if (settings === undefined || name === TOOL_SEARCH_NAME) return
210
+ const always = new Set(settings.alwaysVisible)
211
+ const blocked = new Set(settings.neverSearch)
212
+ always.delete(name)
213
+ blocked.delete(name)
214
+ if (next === 'always') always.add(name)
215
+ if (next === 'blocked') blocked.add(name)
216
+ commit('alwaysVisible', [...always].sort())
217
+ commit('neverSearch', [...blocked].sort())
218
+ }
219
+
220
+ const core = new Set<string>(DEFAULT_CORE_TOOLS)
221
+ return <div className="dts-settings">
222
+ <div className="dts-toolbar">
223
+ <label className="dts-field dts-limit">
224
+ <span>{translate('topK')}</span>
225
+ <input
226
+ className="dts-input"
227
+ type="number"
228
+ min={1}
229
+ max={MAX_RESULT_LIMIT}
230
+ value={limitText}
231
+ disabled={!snapshot.writable}
232
+ onChange={event => setLimitText(event.target.value)}
233
+ onBlur={() => {
234
+ const value = Number.parseInt(limitText, 10)
235
+ if (Number.isSafeInteger(value) && value >= 1 && value <= MAX_RESULT_LIMIT) commit('defaultLimit', value)
236
+ else setLimitText(String(settings?.defaultLimit ?? DEFAULT_RESULT_LIMIT))
237
+ }}
238
+ />
239
+ </label>
240
+ <label className="dts-field">
241
+ <span>{translate('filter')}</span>
242
+ <input className="dts-input" type="search" value={filter} onChange={event => setFilter(event.target.value)} />
243
+ </label>
244
+ <button className="dts-button" type="button" onClick={() => { void refresh() }}>{translate('refresh')}</button>
245
+ </div>
246
+
247
+ <div className="dts-lists">
248
+ <label className="dts-field">
249
+ <span>{translate('alwaysVisible')}</span>
250
+ <textarea
251
+ className="dts-textarea"
252
+ value={alwaysText}
253
+ disabled={!snapshot.writable}
254
+ placeholder={translate('onePerLine')}
255
+ onChange={event => setAlwaysText(event.target.value)}
256
+ onBlur={() => commit('alwaysVisible', namesFromText(alwaysText))}
257
+ />
258
+ </label>
259
+ <label className="dts-field">
260
+ <span>{translate('neverSearch')}</span>
261
+ <textarea
262
+ className="dts-textarea"
263
+ value={blockedText}
264
+ disabled={!snapshot.writable}
265
+ placeholder={translate('onePerLine')}
266
+ onChange={event => setBlockedText(event.target.value)}
267
+ onBlur={() => commit('neverSearch', namesFromText(blockedText).filter(name => name !== TOOL_SEARCH_NAME))}
268
+ />
269
+ </label>
270
+ </div>
271
+
272
+ <section className="dts-field">
273
+ <div className="dts-section-title">{translate('catalog')}</div>
274
+ <div className="dts-table-wrap">
275
+ <table className="dts-table">
276
+ <thead><tr>
277
+ <th className="dts-tool">{translate('tool')}</th>
278
+ <th className="dts-policy">{translate('policy')}</th>
279
+ <th className="dts-selected">{translate('selected')}</th>
280
+ </tr></thead>
281
+ <tbody>
282
+ {visibleTools.map(tool => <tr key={tool.name}>
283
+ <td className="dts-tool">
284
+ <code title={tool.name}>{tool.name}</code>
285
+ {tool.description.length > 0 ? <span className="dts-description" title={tool.description}>{tool.description}</span> : null}
286
+ </td>
287
+ <td className="dts-policy">
288
+ <select
289
+ className="dts-select"
290
+ aria-label={`${translate('policy')}: ${tool.name}`}
291
+ value={tool.name === TOOL_SEARCH_NAME ? 'always' : overrideFor(tool.name, settings ?? {
292
+ defaultLimit: DEFAULT_RESULT_LIMIT,
293
+ alwaysVisible: [],
294
+ neverSearch: [],
295
+ })}
296
+ disabled={!snapshot.writable || tool.name === TOOL_SEARCH_NAME}
297
+ onChange={event => changePolicy(tool.name, event.target.value as ToolPolicy | 'default')}
298
+ >
299
+ <option value="default">{core.has(tool.name) ? `${translate('defaultPolicy')} (${translate('alwaysPolicy')})` : translate('defaultPolicy')}</option>
300
+ <option value="always">{translate('alwaysPolicy')}</option>
301
+ <option value="blocked">{translate('blockedPolicy')}</option>
302
+ </select>
303
+ </td>
304
+ <td
305
+ className="dts-selected"
306
+ data-label={translate('selected')}
307
+ aria-label={`${translate('selected')}: ${tool.selectedBy.length}`}
308
+ >{tool.selectedBy.length}</td>
309
+ </tr>)}
310
+ {catalog !== undefined && visibleTools.length === 0
311
+ ? <tr><td colSpan={3}>{translate('empty')}</td></tr>
312
+ : null}
313
+ </tbody>
314
+ </table>
315
+ </div>
316
+ <div className="dts-status" data-error={status?.error || undefined}>
317
+ {status?.text ?? (catalog === undefined ? translate('loading') : '')}
318
+ </div>
319
+ </section>
320
+ </div>
321
+ }
322
+
323
+ export const inject = ['slots', 'locale', 'settingsScope']
324
+
325
+ export function apply(ctx: ClientContext): void {
326
+ ctx.effect(() => {
327
+ if (document.querySelector('style[data-plugin-css="@anionex/dsh-tool-search"]') !== null) return () => {}
328
+ const tag = document.createElement('style')
329
+ tag.dataset.pluginCss = '@anionex/dsh-tool-search'
330
+ tag.textContent = styles
331
+ document.head.append(tag)
332
+ return () => tag.remove()
333
+ }, 'dsh-tool-search: styles')
334
+ ctx.effect(() => ctx.locale.register('settings.toolSearch', { en, zh }), 'dsh-tool-search: locale')
335
+ const t = ctx.locale.bind('settings.toolSearch') as Translate
336
+ const scope = ctx.settingsScope.bind<ToolSearchSettings>({ namespace: TOOL_SEARCH_SETTINGS_NAMESPACE })
337
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
338
+ name: 'settings.section',
339
+ id: 'tool-search',
340
+ order: 34,
341
+ label: () => t('nav'),
342
+ inject: () => ({ scope, t }),
343
+ }, ToolSearchSettingsSection))
344
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ /** DSH bundle entry for Codex-style deferred tool loading. */
2
+
3
+ import type { Context } from '@deepseek-ai/cordis'
4
+ import type {} from '@deepseek-ai/dsh-agent'
5
+ import type {} from '@deepseek-ai/dsh-system-prompt'
6
+ import type {} from '@deepseek-ai/dsh-tools'
7
+ import { ToolSearchRuntime } from './runtime.ts'
8
+ import { Config, TOOL_SEARCH_SETTINGS_NS } from './settings.ts'
9
+ import type { ToolSearchSettings } from './shared.ts'
10
+ import { installCatalogRoute } from './web.ts'
11
+
12
+ export const name = '@anionex/dsh-tool-search'
13
+ export const inject = ['agents', 'tools', 'systemPrompt', 'settings']
14
+ export { Config }
15
+
16
+ export function apply(ctx: Context, config: ToolSearchSettings): void {
17
+ const settings = ctx.settings.register(TOOL_SEARCH_SETTINGS_NS, Config, {
18
+ base: config,
19
+ applies: 'live',
20
+ })
21
+ const runtime = new ToolSearchRuntime(ctx, settings.get())
22
+ ctx.tools.register(runtime.definition)
23
+ ctx.effect(() => runtime.install(), 'dsh-tool-search: agent visibility')
24
+ ctx.effect(() => settings.watch(next => { runtime.updateSettings(next) }), 'dsh-tool-search: settings')
25
+ installCatalogRoute(ctx, runtime)
26
+ }
27
+
28
+ export { Bm25Index, tokenize } from './bm25.ts'
29
+ export { appendSchemaSearchText, buildCatalog, toolSearchText } from './catalog.ts'
30
+ export { ToolSearchRuntime } from './runtime.ts'
31
+ export { normalizeSettings, ToolPolicyResolver } from './settings.ts'
32
+ export * from './shared.ts'