@moguiyu/dsh-tavily 0.2.7 → 0.3.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/README.md +21 -21
- package/lib/client.js +30 -95
- package/package.json +6 -13
- package/src/backend.js +297 -0
- package/src/client.js +30 -95
- package/src/index.js +30 -157
- package/src/lib.js +65 -0
- package/src/tools.js +632 -0
package/src/client.js
CHANGED
|
@@ -7,7 +7,6 @@ export const id = '@moguiyu/dsh-tavily'
|
|
|
7
7
|
|
|
8
8
|
export function factory(require) {
|
|
9
9
|
const react = require('react')
|
|
10
|
-
const _primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
11
10
|
|
|
12
11
|
const inject = ['slots']
|
|
13
12
|
|
|
@@ -57,18 +56,6 @@ export function factory(require) {
|
|
|
57
56
|
}, react.createElement(SvgIcon, { name: icon, size: 13 }))
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
function Switch({ checked, disabled, label, onChange }) {
|
|
61
|
-
return react.createElement('button', {
|
|
62
|
-
type: 'button',
|
|
63
|
-
role: 'switch',
|
|
64
|
-
'aria-checked': checked === true,
|
|
65
|
-
'aria-label': label,
|
|
66
|
-
disabled: disabled || checked === null,
|
|
67
|
-
onClick: onChange,
|
|
68
|
-
style: { width: 36, height: 20, borderRadius: 10, border: '1px solid var(--dsw-alias-border-l2)', background: checked === true ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-bg-layer-1)', cursor: 'pointer', padding: 0, position: 'relative', flex: 'none', opacity: disabled ? 0.5 : 1 }
|
|
69
|
-
}, react.createElement('span', { style: { position: 'absolute', top: 2, left: checked === true ? 18 : 2, width: 14, height: 14, borderRadius: 7, background: '#fff', transition: 'left .12s ease' } }))
|
|
70
|
-
}
|
|
71
|
-
|
|
72
59
|
function UsageCircle({ percent, size, label, onClick }) {
|
|
73
60
|
const radius = 15
|
|
74
61
|
const circumference = 2 * Math.PI * radius
|
|
@@ -83,9 +70,7 @@ export function factory(require) {
|
|
|
83
70
|
)
|
|
84
71
|
}
|
|
85
72
|
|
|
86
|
-
function
|
|
87
|
-
const [enabled, setEnabled] = react.useState(null)
|
|
88
|
-
const [expanded, setExpanded] = react.useState(false)
|
|
73
|
+
function TavilySettingsCard() {
|
|
89
74
|
const [server, setServer] = react.useState(null)
|
|
90
75
|
const [loadError, setLoadError] = react.useState(null)
|
|
91
76
|
const [strategy, setStrategy] = react.useState('rotate')
|
|
@@ -101,13 +86,6 @@ export function factory(require) {
|
|
|
101
86
|
const [usageError, setUsageError] = react.useState(null)
|
|
102
87
|
|
|
103
88
|
const refresh = react.useCallback(async () => {
|
|
104
|
-
try {
|
|
105
|
-
const response = await fetch('/api/tavily-tool', { cache: 'no-store' })
|
|
106
|
-
const data = await response.json()
|
|
107
|
-
if (data.ok) { setEnabled(data.enabled); setLoadError(null) } else { setLoadError(data.error || 'Failed to load settings') }
|
|
108
|
-
} catch (error) {
|
|
109
|
-
setLoadError(String(error && error.message ? error.message : error))
|
|
110
|
-
}
|
|
111
89
|
try {
|
|
112
90
|
const response = await fetch('/api/tavily-manager', { cache: 'no-store' })
|
|
113
91
|
const data = await response.json()
|
|
@@ -127,27 +105,6 @@ export function factory(require) {
|
|
|
127
105
|
|
|
128
106
|
react.useEffect(() => { refresh() }, [refresh])
|
|
129
107
|
|
|
130
|
-
const toggleEnabled = async () => {
|
|
131
|
-
const next = enabled !== true
|
|
132
|
-
setBusy(true)
|
|
133
|
-
setNotice(null)
|
|
134
|
-
try {
|
|
135
|
-
const response = await fetch('/api/tavily-tool', {
|
|
136
|
-
method: 'POST',
|
|
137
|
-
headers: { 'content-type': 'application/json' },
|
|
138
|
-
body: JSON.stringify({ enabled: next })
|
|
139
|
-
})
|
|
140
|
-
const data = await response.json()
|
|
141
|
-
if (!data.ok) { setNotice({ error: data.error || 'Switch failed' }); return }
|
|
142
|
-
setEnabled(data.enabled)
|
|
143
|
-
setNotice({ ok: data.enabled ? 'Advanced tavily_search tool enabled.' : 'Advanced tavily_search tool disabled — built-in web_search is unchanged.' })
|
|
144
|
-
} catch (error) {
|
|
145
|
-
setNotice({ error: String(error && error.message ? error.message : error) })
|
|
146
|
-
} finally {
|
|
147
|
-
setBusy(false)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
108
|
const saveStrategy = async (next) => {
|
|
152
109
|
setBusy(true)
|
|
153
110
|
setNotice(null)
|
|
@@ -288,46 +245,12 @@ export function factory(require) {
|
|
|
288
245
|
const headStyle = { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid var(--dsw-alias-border-l2)', fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' }
|
|
289
246
|
const cellStyle = { padding: '8px', verticalAlign: 'top' }
|
|
290
247
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return react.createElement('div', { className: expanded ? 'dts-card dts-card-open' : 'dts-card', style: { minWidth: 0 } },
|
|
298
|
-
react.createElement('style', null, '.dts-card{border:.5px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-3);transition:border-color .16s,background .16s}.dts-card:hover{border-color:var(--dsw-alias-label-dimmed)}.dts-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.dts-icon-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}.dts-icon-btn:disabled{opacity:.4;cursor:default}.dts-icon-btn-danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger)}'),
|
|
299
|
-
react.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, padding: '12px 14px', minHeight: 52, boxSizing: 'border-box' } },
|
|
300
|
-
react.createElement('button', {
|
|
301
|
-
type: 'button',
|
|
302
|
-
'aria-expanded': expanded,
|
|
303
|
-
onClick: () => setExpanded((current) => !current),
|
|
304
|
-
style: { flex: 1, minWidth: 0, textAlign: 'left', background: 'none', border: 'none', padding: 0, cursor: 'pointer', display: 'flex', flexDirection: 'column', gap: 2, font: 'inherit', color: 'inherit' }
|
|
305
|
-
},
|
|
306
|
-
react.createElement('strong', { style: { fontSize: 14, fontWeight: 600, lineHeight: '20px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, 'Tavily Search'),
|
|
307
|
-
react.createElement('span', { style: { fontSize: 12, lineHeight: '16px', color: 'var(--dsw-alias-label-tertiary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, 'Advanced tavily_search tool · key rotation · usage')
|
|
308
|
-
),
|
|
309
|
-
react.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6, flex: 'none' } },
|
|
310
|
-
react.createElement(Switch, { checked: enabled, disabled: busy, label: 'Advanced tavily_search tool enabled', onChange: toggleEnabled }),
|
|
311
|
-
react.createElement('button', {
|
|
312
|
-
type: 'button',
|
|
313
|
-
className: 'dts-icon-btn',
|
|
314
|
-
title: expanded ? 'Collapse' : 'Expand',
|
|
315
|
-
'aria-label': expanded ? 'Collapse' : 'Expand',
|
|
316
|
-
onClick: () => setExpanded((current) => !current),
|
|
317
|
-
style: { width: 26, height: 26, borderRadius: 6, border: 'none', background: 'transparent', color: 'var(--dsw-alias-label-tertiary)', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: 0 }
|
|
318
|
-
},
|
|
319
|
-
react.createElement('span', { style: { display: 'inline-flex', transform: expanded ? 'rotate(180deg)' : undefined, transition: 'transform .14s var(--ds-ease-in-out)' } },
|
|
320
|
-
react.createElement(_primitives.IconChevronDownOutline14, { size: 12, 'aria-hidden': 'true' })
|
|
321
|
-
)
|
|
322
|
-
)
|
|
323
|
-
)
|
|
324
|
-
),
|
|
325
|
-
expanded && react.createElement('div', { style: { borderTop: '1px solid var(--dsw-alias-border-l2)', padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 16 } },
|
|
326
|
-
isOff
|
|
327
|
-
? react.createElement('p', { style: { margin: 0, fontSize: 13, color: 'var(--dsw-alias-label-tertiary)' } },
|
|
328
|
-
'The advanced tavily_search tool is off. The built-in web_search keeps its native provider and is never replaced.'
|
|
329
|
-
)
|
|
330
|
-
: react.createElement(react.Fragment, null,
|
|
248
|
+
// The Plugins page draws the title, the icon, and the crumb — the
|
|
249
|
+
// `plugins.bundle.config` contract puts the form alone in the entry — so
|
|
250
|
+
// this renders no card chrome, no second heading, and no collapse.
|
|
251
|
+
return react.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 16, minWidth: 0 } },
|
|
252
|
+
react.createElement('style', null, '.dts-icon-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}.dts-icon-btn:disabled{opacity:.4;cursor:default}.dts-icon-btn-danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger)}'),
|
|
253
|
+
react.createElement(react.Fragment, null,
|
|
331
254
|
react.createElement('p', { style: { margin: 0, fontSize: 13, color: 'var(--dsw-alias-label-tertiary)' } },
|
|
332
255
|
'All keys are listed below; the green dot marks the first (primary) key. The tavily_search tool uses all keys according to the strategy. Built-in web_search is unaffected.'
|
|
333
256
|
),
|
|
@@ -439,21 +362,33 @@ export function factory(require) {
|
|
|
439
362
|
usageError !== null && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(usageError)),
|
|
440
363
|
usage !== null && usage.ok === false && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(usage.error))
|
|
441
364
|
)
|
|
442
|
-
)
|
|
443
365
|
)
|
|
444
366
|
}
|
|
445
367
|
|
|
368
|
+
// The entry the Plugins page renders. The contract asks for two views:
|
|
369
|
+
// `summary` for the one-liner under the title, and `page` for the form with
|
|
370
|
+
// its own save control. The page supplies the title, icon, and crumb, so the
|
|
371
|
+
// summary is a plain string and the page view is the form alone — matching
|
|
372
|
+
// how the shipped cards (WebSearchCard and friends) are written.
|
|
373
|
+
function TavilyCard(props) {
|
|
374
|
+
if (props && props.view === 'summary') {
|
|
375
|
+
return 'Manage Tavily API keys and the key-usage strategy. The built-in web_search is never replaced.'
|
|
376
|
+
}
|
|
377
|
+
return react.createElement(TavilySettingsCard, null)
|
|
378
|
+
}
|
|
379
|
+
|
|
446
380
|
function apply(ctx) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
381
|
+
// ONE slot. 0.1.6-alpha.2 moved plugin configuration to the sidebar
|
|
382
|
+
// Plugins page and retired `settings.plugin.item`. This package no longer
|
|
383
|
+
// registers a settings namespace, so the retired keyed slot has no key it
|
|
384
|
+
// could claim. On rc.7 through 0.1.6-alpha.1 the tools and routes work but
|
|
385
|
+
// this card has no host — see `docs/agents/plugin-design.md`.
|
|
386
|
+
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
387
|
+
name: 'plugins.bundle.config',
|
|
388
|
+
// Keyed by the bundle's package name. The Plugins page renders this on
|
|
389
|
+
// the @moguiyu/dsh-tavily bundle page, between its description and rows.
|
|
390
|
+
key: '@moguiyu/dsh-tavily',
|
|
391
|
+
id: '@moguiyu/dsh-tavily'
|
|
457
392
|
}, TavilyCard))
|
|
458
393
|
}
|
|
459
394
|
|
package/src/index.js
CHANGED
|
@@ -1,170 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@moguiyu/dsh-tavily` combined
|
|
3
|
-
* tool plus the local HTTP backend for usage and key management. The
|
|
4
|
-
* implementations live in the two standalone packages and are composed here,
|
|
5
|
-
* so there is exactly one copy of each:
|
|
2
|
+
* `@moguiyu/dsh-tavily` — the combined plugin, and now the only package.
|
|
6
3
|
*
|
|
7
|
-
*
|
|
8
|
-
* - backend half — `@moguiyu/dsh-tavily-backend` {@link installBackend}.
|
|
4
|
+
* Both halves live here, so there is no cross-package contract left to skew:
|
|
9
5
|
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
6
|
+
* - the model tools (`tools.js`) — `tavily_search`, `tavily_extract`,
|
|
7
|
+
* `tavily_map`, `tavily_crawl`, with key rotation and failover on 401/429;
|
|
8
|
+
* - key and usage management (`backend.js`) — `/api/tavily-usage` and
|
|
9
|
+
* `/api/tavily-manager`;
|
|
10
|
+
* - the browser card (`client.js`) — keys, usage, and strategy.
|
|
15
11
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* that is gone on purpose. A row restart on 0.1.6 re-mounts the fiber into a
|
|
20
|
-
* scope the agent does not see, so the tools ended up registered nowhere and
|
|
21
|
-
* stayed missing for every session until DSH restarted (AGENTS.md §9 gate 5).
|
|
22
|
-
* Do not reintroduce the restart.
|
|
12
|
+
* Composing the plugin registers the tools. There is NO tool-level on/off:
|
|
13
|
+
* the Plugins page's own bundle toggle is the only one. Two earlier designs
|
|
14
|
+
* are gone deliberately, both recorded in `docs/agents/verification.md`:
|
|
23
15
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
16
|
+
* 1. A row restart. On 0.1.6 it re-mounts the fiber into a scope the agent
|
|
17
|
+
* does not see, so the tools ended up registered nowhere and stayed
|
|
18
|
+
* missing, process-wide, until dsh restarted.
|
|
19
|
+
* 2. A tool-level switch split across packages. The combined package needed a
|
|
20
|
+
* disposer from the tool half, but its dependency range still allowed the
|
|
21
|
+
* old tool half, so `installTavilyTool` returned `undefined` and turning
|
|
22
|
+
* the switch off threw. One package removes the range as a failure mode.
|
|
23
|
+
*
|
|
24
|
+
* The built-in `web_search` tool is never replaced: no web search provider is
|
|
25
|
+
* registered, `web.searchProvider` / `DSH_WEB_SEARCH_PROVIDER` are never
|
|
26
|
+
* rewritten, and `ctx.web` is untouched.
|
|
28
27
|
*/
|
|
29
|
-
import
|
|
30
|
-
import {
|
|
31
|
-
import { installBackend, readToolState, restoreToolState, writeToolState } from '@moguiyu/dsh-tavily-backend'
|
|
28
|
+
import { installTavilyTool } from './tools.js'
|
|
29
|
+
import { installBackend } from './backend.js'
|
|
32
30
|
|
|
33
31
|
export const name = 'dsh-tavily'
|
|
34
32
|
|
|
35
33
|
export const inject = ['tools', 'webServer', 'credentials', 'systemPrompt']
|
|
36
34
|
|
|
37
|
-
export
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Settings namespace owned by this package. It is the join key between the
|
|
43
|
-
* Host half and the browser card: the Plugins configuration tab serves this
|
|
44
|
-
* namespace and dispatches the card registered under the same key. A plain
|
|
45
|
-
* lowercase-hyphenated string, valid on every supported host line: rc.7/rc.8
|
|
46
|
-
* treat the namespace as a branded string (runtime-identical), and 0.1.2
|
|
47
|
-
* validates and brands it on registration (`settingsNamespace()` was
|
|
48
|
-
* removed upstream).
|
|
49
|
-
*/
|
|
50
|
-
export const TAVILY_NS = 'tavily-search'
|
|
51
|
-
|
|
52
|
-
export function apply(ctx, config) {
|
|
53
|
-
// The switch value this fiber is acting on. Every write path updates it
|
|
54
|
-
// before (un)registering, so a watcher re-fired afterwards no-ops.
|
|
55
|
-
let lastApplied = isToolEnabled(config)
|
|
56
|
-
// Settings seam state: the attached provider (null on hosts without one)
|
|
57
|
-
// and the authoritative switch source — the resolved section while the
|
|
58
|
-
// provider is attached, the composition entry otherwise.
|
|
59
|
-
let settingsProvider = null
|
|
60
|
-
let settingsSource = () => config
|
|
61
|
-
// The live registration of the tool half, or null while it is off.
|
|
62
|
-
let disposeTools = null
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Add or remove the tool half to match `enabled`. This is the whole switch:
|
|
66
|
-
* `installTavilyTool` hands back a disposer covering its tools and prompt
|
|
67
|
-
* sections, so turning the capability off is a disposal rather than a row
|
|
68
|
-
* restart. See the module header for why the restart is gone.
|
|
69
|
-
*/
|
|
70
|
-
function setTools(enabled) {
|
|
71
|
-
if (enabled) {
|
|
72
|
-
if (disposeTools === null) disposeTools = installTavilyTool(ctx)
|
|
73
|
-
return
|
|
74
|
-
}
|
|
75
|
-
if (disposeTools !== null) {
|
|
76
|
-
const dispose = disposeTools
|
|
77
|
-
disposeTools = null
|
|
78
|
-
dispose()
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Seed from the composition entry / persisted switch, and hand the fiber the
|
|
83
|
-
// teardown so unloading the row leaves nothing registered.
|
|
84
|
-
setTools(lastApplied)
|
|
85
|
-
ctx.effect(() => () => { setTools(false) })
|
|
86
|
-
|
|
87
|
-
/** Persist the switch, then (un)register the tool half in place. */
|
|
88
|
-
async function applySwitchLocal(enabled) {
|
|
89
|
-
if (enabled === lastApplied) return
|
|
90
|
-
const previous = readToolState()
|
|
91
|
-
lastApplied = enabled
|
|
92
|
-
writeToolState(enabled)
|
|
93
|
-
try {
|
|
94
|
-
setTools(enabled)
|
|
95
|
-
} catch (error) {
|
|
96
|
-
lastApplied = previous !== null ? previous.enabled : (config.enabled !== false)
|
|
97
|
-
restoreToolState(previous)
|
|
98
|
-
setTools(lastApplied)
|
|
99
|
-
throw error
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Re-judge the switch from a resolved settings value (either seam's shape). */
|
|
104
|
-
function applySwitchValue(value) {
|
|
105
|
-
const nextEnabled = value !== null && typeof value === 'object' && typeof value.enabled === 'boolean'
|
|
106
|
-
? value.enabled
|
|
107
|
-
: false
|
|
108
|
-
applySwitchLocal(nextEnabled).catch((error) => {
|
|
109
|
-
ctx.logger.warn('tavily-search: applying settings switch failed: %s', error instanceof Error ? error.message : String(error))
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// First-class settings integration: install the `tavily-search` section so
|
|
114
|
-
// the Plugins configuration surface serves it and pairs this package's
|
|
115
|
-
// card. Runtime-optional on purpose: the row boots (state-file path) even
|
|
116
|
-
// on hosts that never provide a settings service. Two host seams are
|
|
117
|
-
// supported by feature detection, so ONE package version covers both host
|
|
118
|
-
// lines:
|
|
119
|
-
//
|
|
120
|
-
// - 0.1.2 (`installSection`) — the row config is the composition entry
|
|
121
|
-
// (base layer while a provider is attached, fallback value when one
|
|
122
|
-
// detaches); `onChange` re-judges the switch on attach, on detach, and
|
|
123
|
-
// on every committed change.
|
|
124
|
-
// - rc.7/rc.8 (`register`) — the row config is the composition base layer
|
|
125
|
-
// and the scope watcher re-judges the switch on every committed change
|
|
126
|
-
// (`applies: 'restart'`, the 0.2.0 behavior).
|
|
127
|
-
ctx.inject(['settings'], (settingsCtx) => {
|
|
128
|
-
const provider = settingsCtx.settings
|
|
129
|
-
try {
|
|
130
|
-
if (typeof provider.installSection === 'function') {
|
|
131
|
-
provider.installSection(ctx, TAVILY_NS, Config, config, {
|
|
132
|
-
setSource: (current) => { settingsSource = current },
|
|
133
|
-
onChange: () => { applySwitchValue(settingsSource()) },
|
|
134
|
-
})
|
|
135
|
-
settingsProvider = provider
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
if (typeof provider.register === 'function') {
|
|
139
|
-
const scope = provider.register(TAVILY_NS, Config, {
|
|
140
|
-
base: { enabled: config.enabled },
|
|
141
|
-
applies: 'restart',
|
|
142
|
-
})
|
|
143
|
-
if (scope !== undefined && scope !== null && typeof scope.watch === 'function') {
|
|
144
|
-
scope.watch((next) => { applySwitchValue(next) })
|
|
145
|
-
settingsProvider = provider
|
|
146
|
-
} else {
|
|
147
|
-
ctx.logger.warn('tavily-search: settings scope exposes no watcher; using the state-file path')
|
|
148
|
-
}
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
-
ctx.logger.warn('tavily-search: settings service exposes neither installSection nor register; using the state-file path')
|
|
152
|
-
} catch (error) {
|
|
153
|
-
ctx.logger.warn('tavily-search: settings section installation failed: %s', error instanceof Error ? error.message : String(error))
|
|
154
|
-
}
|
|
155
|
-
})
|
|
35
|
+
export function apply(ctx) {
|
|
36
|
+
const disposeTools = installTavilyTool(ctx)
|
|
37
|
+
installBackend(ctx)
|
|
156
38
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// settings service is attached (its watcher persists and re-applies the
|
|
162
|
-
// switch in place), or the plain state-file path otherwise.
|
|
163
|
-
if (settingsProvider !== null) {
|
|
164
|
-
await settingsProvider.update(TAVILY_NS, { enabled })
|
|
165
|
-
} else {
|
|
166
|
-
await applySwitchLocal(enabled)
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
})
|
|
39
|
+
// The fiber owns the teardown: unloading the row — including the Plugins
|
|
40
|
+
// page's runtime unload on 0.1.6-alpha.2 — unregisters every tool and
|
|
41
|
+
// prompt section this row added.
|
|
42
|
+
ctx.effect(() => () => { disposeTools() })
|
|
170
43
|
}
|
package/src/lib.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the Tavily backend (exported for tests).
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
5
|
+
|
|
6
|
+
export const STRATEGIES = ['rotate', 'low-usage-first', 'high-usage-first']
|
|
7
|
+
|
|
8
|
+
export function isValidStrategy(value) {
|
|
9
|
+
return STRATEGIES.includes(value)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Mask a key for display: `first12…last4`; comma-separated lists are masked
|
|
14
|
+
* per part. Never reveals more than a fingerprint.
|
|
15
|
+
*/
|
|
16
|
+
export function maskValue(value) {
|
|
17
|
+
if (typeof value !== 'string' || value.length === 0) return ''
|
|
18
|
+
return value.split(',').map((part) => {
|
|
19
|
+
const p = part.trim()
|
|
20
|
+
if (p.length <= 12) return '••••'
|
|
21
|
+
return p.slice(0, 12) + '…' + p.slice(-4)
|
|
22
|
+
}).join(', ')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Split a comma-separated credential value into trimmed, deduped keys. */
|
|
26
|
+
export function parseKeyList(value) {
|
|
27
|
+
if (typeof value !== 'string') return []
|
|
28
|
+
const seen = new Set()
|
|
29
|
+
const out = []
|
|
30
|
+
for (const part of value.split(',')) {
|
|
31
|
+
const key = part.trim()
|
|
32
|
+
if (key.length > 0 && !seen.has(key)) {
|
|
33
|
+
seen.add(key)
|
|
34
|
+
out.push(key)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Order keys according to a strategy. `usageOf(key)` returns a number or null
|
|
42
|
+
* (unknown usage is treated as 0). `rotate` keeps the given order.
|
|
43
|
+
*/
|
|
44
|
+
export function orderKeys(values, strategy, usageOf) {
|
|
45
|
+
if (strategy === 'rotate') return [...values]
|
|
46
|
+
const usageOfKey = (value) => {
|
|
47
|
+
const usage = usageOf(value)
|
|
48
|
+
return usage !== null && typeof usage === 'number' ? usage : 0
|
|
49
|
+
}
|
|
50
|
+
const sorted = [...values]
|
|
51
|
+
sorted.sort((a, b) => strategy === 'low-usage-first'
|
|
52
|
+
? usageOfKey(a) - usageOfKey(b)
|
|
53
|
+
: usageOfKey(b) - usageOfKey(a))
|
|
54
|
+
return sorted
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Read a JSON object file; `fallback` on missing or unparsable content. */
|
|
58
|
+
export function readJsonFile(path, fallback) {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'))
|
|
61
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback
|
|
62
|
+
} catch {
|
|
63
|
+
return fallback
|
|
64
|
+
}
|
|
65
|
+
}
|