@goodandready/dsh-clinebot 0.5.28 → 0.5.30
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/CHANGELOG.md +20 -0
- package/lib/client.js +629 -586
- package/lib/config.js +9 -0
- package/lib/index.js +31 -0
- package/lib/key-discovery.js +109 -0
- package/lib/multimodal-budget.js +108 -0
- package/lib/proxy-token.js +2 -2
- package/lib/routes/accounts.js +17 -0
- package/lib/routes/proxy.js +11 -6
- package/lib/web-search.js +82 -0
- package/package.json +1 -1
package/lib/config.js
CHANGED
|
@@ -43,6 +43,12 @@ export const Config = z.object({
|
|
|
43
43
|
.description('User-selected visible models in chat model picker (empty means all supported models).').volatile(),
|
|
44
44
|
showSidebarQuota: z.boolean().default(true)
|
|
45
45
|
.description('Display compact quota meter in DSH sidebar footer.').volatile(),
|
|
46
|
+
webSearch: z.boolean().default(true)
|
|
47
|
+
.description('Enable native DSH web search provider via ClineBot.').volatile(),
|
|
48
|
+
offloadSeenImages: z.boolean().default(true)
|
|
49
|
+
.description('Offload previously seen images in conversation history to optimize prompt cache.').volatile(),
|
|
50
|
+
maxPayloadMb: z.number().default(40)
|
|
51
|
+
.description('Maximum multimodal payload budget in MB before evicting oldest images.').volatile(),
|
|
46
52
|
enabledModels: z.array(z.string()).default([])
|
|
47
53
|
.description('Deprecated: preserved for backwards compatibility with earlier versions.'),
|
|
48
54
|
dynamicModels: z.array(z.object({
|
|
@@ -212,6 +218,9 @@ export function publicConfig(cfg) {
|
|
|
212
218
|
filterModelsByPlan,
|
|
213
219
|
visibleModels,
|
|
214
220
|
showSidebarQuota: cfg?.showSidebarQuota !== false,
|
|
221
|
+
webSearch: cfg?.webSearch !== false,
|
|
222
|
+
offloadSeenImages: cfg?.offloadSeenImages !== false,
|
|
223
|
+
maxPayloadMb: Number(cfg?.maxPayloadMb) || 40,
|
|
215
224
|
enabledModels: activeIds,
|
|
216
225
|
timeoutMs: Number(cfg?.timeoutMs) || DEFAULT_TIMEOUT_MS,
|
|
217
226
|
smokeTimeoutMs: Number(cfg?.smokeTimeoutMs) || DEFAULT_SMOKE_TIMEOUT_MS,
|
package/lib/index.js
CHANGED
|
@@ -25,6 +25,8 @@ import { registerAuthRoutes } from './routes/auth.js'
|
|
|
25
25
|
import { registerProxyRoutes } from './routes/proxy.js'
|
|
26
26
|
import { startLoopbackProxyServer, stopLoopbackProxyServer } from './proxy-server.js'
|
|
27
27
|
import { registerSlashCommand } from './slash-command.js'
|
|
28
|
+
import { ClineBotSearchProvider, CLINEBOT_SEARCH_PROVIDER_ID } from './web-search.js'
|
|
29
|
+
import { resolveAccountPool } from './account-pool.js'
|
|
28
30
|
import { recordUsage, loadStats, flushStats, resolvePath, switchStatsStorage } from './stats-storage.js'
|
|
29
31
|
|
|
30
32
|
export const name = '@goodandready/dsh-clinebot'
|
|
@@ -468,4 +470,33 @@ export function apply(ctx, config) {
|
|
|
468
470
|
ctx.effect(() => () => unlisten?.(), 'dsh-clinebot: llm/stream failover')
|
|
469
471
|
}
|
|
470
472
|
}
|
|
473
|
+
|
|
474
|
+
// Native DSH WebSearchProvider over ctx.web (#233)
|
|
475
|
+
if (typeof ctx.inject === 'function') {
|
|
476
|
+
ctx.inject(['web'], (webCtx) => {
|
|
477
|
+
const cfg = publicConfig(live())
|
|
478
|
+
if (cfg.webSearch === false) return
|
|
479
|
+
const web = webCtx?.get?.('web')
|
|
480
|
+
if (web && typeof web.registerSearchProvider === 'function') {
|
|
481
|
+
const searchProvider = new ClineBotSearchProvider({
|
|
482
|
+
getBaseUrl: () => publicConfig(live()).baseUrl,
|
|
483
|
+
getApiKey: async () => {
|
|
484
|
+
const pool = await resolveAccountPool(ctx, live())
|
|
485
|
+
const active = pool.find((a) => a.present && a.value)
|
|
486
|
+
return active?.value || ''
|
|
487
|
+
},
|
|
488
|
+
})
|
|
489
|
+
try {
|
|
490
|
+
const unreg = web.registerSearchProvider(CLINEBOT_SEARCH_PROVIDER_ID, searchProvider)
|
|
491
|
+
if (typeof ctx.effect === 'function') {
|
|
492
|
+
ctx.effect(() => () => {
|
|
493
|
+
try { if (typeof unreg === 'function') unreg() } catch {}
|
|
494
|
+
}, 'dsh-clinebot: web search provider')
|
|
495
|
+
}
|
|
496
|
+
} catch (e) {
|
|
497
|
+
ctx?.logger?.warn?.('[dsh-clinebot] Failed to register web search provider: ' + (e?.message || e))
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
}
|
|
471
502
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
|
|
5
|
+
const CLINE_KEY_REGEX = /cline_[A-Za-z0-9_\-]{16,}/g
|
|
6
|
+
|
|
7
|
+
function maskApiKey(key) {
|
|
8
|
+
if (!key || typeof key !== 'string') return ''
|
|
9
|
+
if (key.length <= 12) return '••••••••'
|
|
10
|
+
return key.slice(0, 8) + '••••' + key.slice(-4)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function scanFileForKeys(filePath, sourceName) {
|
|
14
|
+
const results = []
|
|
15
|
+
try {
|
|
16
|
+
if (!fs.existsSync(filePath)) return results
|
|
17
|
+
const stat = fs.statSync(filePath)
|
|
18
|
+
if (!stat.isFile() || stat.size > 5 * 1024 * 1024) return results
|
|
19
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
20
|
+
const matches = content.match(CLINE_KEY_REGEX)
|
|
21
|
+
if (matches) {
|
|
22
|
+
for (const m of matches) {
|
|
23
|
+
results.push({
|
|
24
|
+
source: sourceName,
|
|
25
|
+
key: m,
|
|
26
|
+
maskedKey: maskApiKey(m),
|
|
27
|
+
path: filePath,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// quiet on unreadable files
|
|
33
|
+
}
|
|
34
|
+
return results
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function discoverLocalClineKeys() {
|
|
38
|
+
const discovered = []
|
|
39
|
+
const seenKeys = new Set()
|
|
40
|
+
|
|
41
|
+
const addKey = (item) => {
|
|
42
|
+
if (!item?.key || seenKeys.has(item.key)) return
|
|
43
|
+
seenKeys.add(item.key)
|
|
44
|
+
discovered.push(item)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 1. Check environment variables
|
|
48
|
+
for (const envName of ['CLINE_API_KEY', 'CLINEPASS_API_KEY']) {
|
|
49
|
+
const val = process.env[envName]
|
|
50
|
+
if (val && typeof val === 'string' && val.startsWith('cline_')) {
|
|
51
|
+
addKey({
|
|
52
|
+
source: `Environment (${envName})`,
|
|
53
|
+
key: val,
|
|
54
|
+
maskedKey: maskApiKey(val),
|
|
55
|
+
path: `$${envName}`,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2. Candidate directories & file paths across OSes
|
|
61
|
+
const home = os.homedir()
|
|
62
|
+
const candidateFiles = [
|
|
63
|
+
// CLI configs
|
|
64
|
+
{ path: path.join(home, '.cline', 'config.json'), source: 'Cline CLI' },
|
|
65
|
+
{ path: path.join(home, '.cline', 'data', 'settings.json'), source: 'Cline CLI Data' },
|
|
66
|
+
{ path: path.join(home, '.cline', 'settings.json'), source: 'Cline CLI Settings' },
|
|
67
|
+
{ path: path.join(home, '.config', 'cline', 'config.json'), source: 'Cline Config' },
|
|
68
|
+
{ path: path.join(home, '.cline.json'), source: 'Cline Dotfile' },
|
|
69
|
+
|
|
70
|
+
// VS Code Extension Global Storage (Linux, macOS, Windows)
|
|
71
|
+
{ path: path.join(home, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_custom_modes.json'), source: 'VS Code Extension' },
|
|
72
|
+
{ path: path.join(home, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'state.vscdb'), source: 'VS Code Extension Storage' },
|
|
73
|
+
{ path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_custom_modes.json'), source: 'VS Code Extension (macOS)' },
|
|
74
|
+
{ path: path.join(home, 'AppData', 'Roaming', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_custom_modes.json'), source: 'VS Code Extension (Windows)' },
|
|
75
|
+
|
|
76
|
+
// Cursor Extension Global Storage
|
|
77
|
+
{ path: path.join(home, '.config', 'Cursor', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_custom_modes.json'), source: 'Cursor Extension' },
|
|
78
|
+
{ path: path.join(home, '.config', 'Cursor', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'state.vscdb'), source: 'Cursor Extension Storage' },
|
|
79
|
+
{ path: path.join(home, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_custom_modes.json'), source: 'Cursor Extension (macOS)' },
|
|
80
|
+
{ path: path.join(home, 'AppData', 'Roaming', 'Cursor', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_custom_modes.json'), source: 'Cursor Extension (Windows)' },
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
for (const candidate of candidateFiles) {
|
|
84
|
+
const hits = scanFileForKeys(candidate.path, candidate.source)
|
|
85
|
+
for (const h of hits) {
|
|
86
|
+
addKey(h)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Also check if any file in ~/.cline/ contains cline_ keys
|
|
91
|
+
const clineDir = path.join(home, '.cline')
|
|
92
|
+
try {
|
|
93
|
+
if (fs.existsSync(clineDir)) {
|
|
94
|
+
const entries = fs.readdirSync(clineDir)
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const full = path.join(clineDir, entry)
|
|
97
|
+
try {
|
|
98
|
+
const st = fs.statSync(full)
|
|
99
|
+
if (st.isFile()) {
|
|
100
|
+
const hits = scanFileForKeys(full, 'Cline CLI (~/.cline)')
|
|
101
|
+
for (const h of hits) addKey(h)
|
|
102
|
+
}
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch {}
|
|
107
|
+
|
|
108
|
+
return discovered
|
|
109
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multimodal Request Budgeting & Prompt Cache Image Offload (Issue #234)
|
|
3
|
+
*
|
|
4
|
+
* 1. Offloads previously seen images from earlier turns in the conversation history
|
|
5
|
+
* into stable text placeholders, preserving model KV/prompt cache prefixes.
|
|
6
|
+
* 2. Enforces a strict payload byte ceiling (default 40MB) to prevent upstream HTTP 413
|
|
7
|
+
* rejections and memory spikes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const DEFAULT_MAX_PAYLOAD_BYTES = 40 * 1024 * 1024 // 40 MB
|
|
11
|
+
|
|
12
|
+
function estimateImageBlockBytes(block) {
|
|
13
|
+
if (!block || typeof block !== 'object') return 0
|
|
14
|
+
if (block.type === 'image_url' && typeof block.image_url?.url === 'string') {
|
|
15
|
+
return block.image_url.url.length
|
|
16
|
+
}
|
|
17
|
+
if (block.type === 'image' && typeof block.source?.data === 'string') {
|
|
18
|
+
return block.source.data.length
|
|
19
|
+
}
|
|
20
|
+
return 0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeTextPlaceholder(reason = 'analyzed') {
|
|
24
|
+
return {
|
|
25
|
+
type: 'text',
|
|
26
|
+
text: reason === 'cache'
|
|
27
|
+
? '[Image: previously analyzed visual attachment retained in context]'
|
|
28
|
+
: '[Image: attachment pruned to satisfy request payload budget]',
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function optimizePromptCacheAndBudget(messages, options = {}) {
|
|
33
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
34
|
+
return { messages: messages || [], offloadedCount: 0, totalImageBytes: 0 }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const offloadSeen = options.offloadSeenImages !== false
|
|
38
|
+
const maxBytes = Number(options.maxPayloadBytes) || DEFAULT_MAX_PAYLOAD_BYTES
|
|
39
|
+
|
|
40
|
+
// Find index of the latest assistant message in the history
|
|
41
|
+
let lastAssistantIdx = -1
|
|
42
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
43
|
+
if (messages[i]?.role === 'assistant') {
|
|
44
|
+
lastAssistantIdx = i
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let offloadedCount = 0
|
|
50
|
+
const clone = messages.map(m => {
|
|
51
|
+
if (!m || typeof m !== 'object') return m
|
|
52
|
+
if (Array.isArray(m.content)) {
|
|
53
|
+
return { ...m, content: [...m.content] }
|
|
54
|
+
}
|
|
55
|
+
return { ...m }
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Phase 1: Offload seen images (appear before lastAssistantIdx)
|
|
59
|
+
if (offloadSeen && lastAssistantIdx > 0) {
|
|
60
|
+
for (let i = 0; i < lastAssistantIdx; i++) {
|
|
61
|
+
const msg = clone[i]
|
|
62
|
+
if (Array.isArray(msg?.content)) {
|
|
63
|
+
for (let j = 0; j < msg.content.length; j++) {
|
|
64
|
+
const item = msg.content[j]
|
|
65
|
+
if (item?.type === 'image_url' || item?.type === 'image') {
|
|
66
|
+
msg.content[j] = makeTextPlaceholder('cache')
|
|
67
|
+
offloadedCount += 1
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Phase 2: Calculate total remaining image bytes
|
|
75
|
+
const remainingImages = []
|
|
76
|
+
let totalBytes = 0
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < clone.length; i++) {
|
|
79
|
+
const msg = clone[i]
|
|
80
|
+
if (Array.isArray(msg?.content)) {
|
|
81
|
+
for (let j = 0; j < msg.content.length; j++) {
|
|
82
|
+
const item = msg.content[j]
|
|
83
|
+
if (item?.type === 'image_url' || item?.type === 'image') {
|
|
84
|
+
const bytes = estimateImageBlockBytes(item)
|
|
85
|
+
totalBytes += bytes
|
|
86
|
+
remainingImages.push({ msgIdx: i, blockIdx: j, bytes })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Phase 3: Budget check - if over maxBytes, evict oldest remaining images
|
|
93
|
+
if (totalBytes > maxBytes && remainingImages.length > 0) {
|
|
94
|
+
for (const img of remainingImages) {
|
|
95
|
+
if (totalBytes <= maxBytes) break
|
|
96
|
+
const msg = clone[img.msgIdx]
|
|
97
|
+
msg.content[img.blockIdx] = makeTextPlaceholder('budget')
|
|
98
|
+
totalBytes -= img.bytes
|
|
99
|
+
offloadedCount += 1
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
messages: clone,
|
|
105
|
+
offloadedCount,
|
|
106
|
+
totalImageBytes: totalBytes,
|
|
107
|
+
}
|
|
108
|
+
}
|
package/lib/proxy-token.js
CHANGED
|
@@ -14,9 +14,9 @@ export const LOCAL_PROXY_KEY_ENV = 'CLINEBOT_LOCAL_PROXY_TOKEN'
|
|
|
14
14
|
let cachedToken = null
|
|
15
15
|
|
|
16
16
|
const require = createRequire(import.meta.url)
|
|
17
|
-
let packageVersion = '0.5.
|
|
17
|
+
let packageVersion = '0.5.30'
|
|
18
18
|
try {
|
|
19
|
-
packageVersion = require('../package.json').version || '0.5.
|
|
19
|
+
packageVersion = require('../package.json').version || '0.5.30'
|
|
20
20
|
} catch (err) {
|
|
21
21
|
/* fallback to default version if manifest is unreadable */
|
|
22
22
|
}
|
package/lib/routes/accounts.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { writeJson, readBody } from '../http.js'
|
|
2
|
+
import { discoverLocalClineKeys } from '../key-discovery.js'
|
|
2
3
|
import { assertTrustedSettingsRequest } from '../access.js'
|
|
3
4
|
import { publicConfig, plainConfig, Config } from '../config.js'
|
|
4
5
|
import { upsertPiAiProvider, removePiAiProvider } from '../provider-sync.js'
|
|
@@ -336,4 +337,20 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
336
337
|
}
|
|
337
338
|
},
|
|
338
339
|
}), 'dsh-clinebot: /unregister')
|
|
340
|
+
|
|
341
|
+
// GET /dsh-clinebot/keys/discovered — scan local Cline CLI and VS Code extension keys (#232)
|
|
342
|
+
ctx.effect(() => ctx.webServer.register({
|
|
343
|
+
kind: 'exact',
|
|
344
|
+
path: '/dsh-clinebot/keys/discovered',
|
|
345
|
+
handler: async (req, res) => {
|
|
346
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
347
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
348
|
+
try {
|
|
349
|
+
const discovered = discoverLocalClineKeys()
|
|
350
|
+
writeJson(res, 200, { ok: true, discovered })
|
|
351
|
+
} catch (err) {
|
|
352
|
+
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
}), 'dsh-clinebot: /keys/discovered')
|
|
339
356
|
}
|
package/lib/routes/proxy.js
CHANGED
|
@@ -21,13 +21,10 @@ import { resolveAccountPool } from '../account-pool.js'
|
|
|
21
21
|
import { resolveSessionAccount, markAccountCooldown } from '../session-router.js'
|
|
22
22
|
import { recordUsage } from '../stats-storage.js'
|
|
23
23
|
import { getAllModels, DEFAULT_MODEL_ID } from '../models.js'
|
|
24
|
+
import { optimizePromptCacheAndBudget } from '../multimodal-budget.js'
|
|
24
25
|
import {
|
|
25
|
-
getLocalProxyToken,
|
|
26
|
-
|
|
27
|
-
isAuthorizedProxyRequest,
|
|
28
|
-
USER_AGENT,
|
|
29
|
-
sendProxyTerminalError,
|
|
30
|
-
extractUsageMetrics,
|
|
26
|
+
getLocalProxyToken, hasDisallowedBrowserOrigin, isAuthorizedProxyRequest,
|
|
27
|
+
USER_AGENT, sendProxyTerminalError, extractUsageMetrics
|
|
31
28
|
} from '../proxy-token.js'
|
|
32
29
|
|
|
33
30
|
export { hasDisallowedBrowserOrigin, isAuthorizedProxyRequest, USER_AGENT }
|
|
@@ -85,6 +82,14 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
|
|
|
85
82
|
body.reasoning_effort = pub.modelReasoningDefaults[body.model]
|
|
86
83
|
}
|
|
87
84
|
|
|
85
|
+
if (Array.isArray(body?.messages) && body.messages.length > 0) {
|
|
86
|
+
const budgetResult = optimizePromptCacheAndBudget(body.messages, {
|
|
87
|
+
offloadSeenImages: pub.offloadSeenImages !== false,
|
|
88
|
+
maxPayloadBytes: (Number(pub.maxPayloadMb) || 40) * 1024 * 1024,
|
|
89
|
+
})
|
|
90
|
+
body.messages = budgetResult.messages
|
|
91
|
+
}
|
|
92
|
+
|
|
88
93
|
const isStreaming = Boolean(body.stream)
|
|
89
94
|
const configuredAccounts = pool.filter(a => a.present && a.value)
|
|
90
95
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClineBot Native WebSearchProvider for ctx.web (Issue #233)
|
|
3
|
+
*
|
|
4
|
+
* Implements DSH WebSearchProvider contract so the model-facing
|
|
5
|
+
* web_search tool can search the web via ClinePass upstream.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const CLINEBOT_SEARCH_PROVIDER_ID = 'clinebot'
|
|
9
|
+
|
|
10
|
+
function clampNumResults(maxResults) {
|
|
11
|
+
const n = Number(maxResults)
|
|
12
|
+
if (Number.isNaN(n) || n <= 0) return 5
|
|
13
|
+
return Math.max(1, Math.min(10, Math.round(n)))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toSource(result) {
|
|
17
|
+
if (!result || typeof result !== 'object') return null
|
|
18
|
+
const url = String(result.url || '').trim()
|
|
19
|
+
if (!url) return null
|
|
20
|
+
const title = String(result.title || '').trim()
|
|
21
|
+
const snippet = String(result.snippet || '').trim()
|
|
22
|
+
return {
|
|
23
|
+
url,
|
|
24
|
+
...(title ? { title } : {}),
|
|
25
|
+
...(snippet ? { snippet } : {}),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ClineBotSearchProvider {
|
|
30
|
+
constructor(deps) {
|
|
31
|
+
this.deps = deps || {}
|
|
32
|
+
this.id = CLINEBOT_SEARCH_PROVIDER_ID
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
available() {
|
|
36
|
+
const base = this.deps.getBaseUrl ? this.deps.getBaseUrl() : 'https://api.cline.bot/api/v1'
|
|
37
|
+
return Boolean(base && typeof base === 'string')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async search(request, signal) {
|
|
41
|
+
if (signal?.aborted) {
|
|
42
|
+
const err = new Error('ClineBot web search aborted')
|
|
43
|
+
err.name = 'AbortError'
|
|
44
|
+
throw err
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const query = String(request?.query || '').trim()
|
|
48
|
+
if (!query) return { sources: [] }
|
|
49
|
+
|
|
50
|
+
const baseUrl = (this.deps.getBaseUrl ? this.deps.getBaseUrl() : 'https://api.cline.bot/api/v1').replace(/\/+$/, '')
|
|
51
|
+
const key = this.deps.getApiKey ? await this.deps.getApiKey() : ''
|
|
52
|
+
const endpoint = `${baseUrl}/alpha/web-search`
|
|
53
|
+
const numResults = clampNumResults(request?.maxResults)
|
|
54
|
+
|
|
55
|
+
const fetchImpl = this.deps.fetchImpl || fetch
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const resp = await fetchImpl(endpoint, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': 'application/json',
|
|
62
|
+
...(key ? { Authorization: `Bearer ${key}` } : {}),
|
|
63
|
+
'x-cli-environment': 'production',
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify({ query, numResults }),
|
|
66
|
+
signal,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
if (!resp.ok) {
|
|
70
|
+
return { sources: [] }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const data = await resp.json().catch(() => ({}))
|
|
74
|
+
const rawResults = Array.isArray(data?.results) ? data.results : []
|
|
75
|
+
const sources = rawResults.map(toSource).filter(Boolean)
|
|
76
|
+
return { sources }
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err.name === 'AbortError') throw err
|
|
79
|
+
return { sources: [] }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.30",
|
|
4
4
|
"description": "DeepSeek Harness companion for ClineBot / ClinePass: dynamic subscription models sync, quota exhaustion warnings, session metrics, dedicated settings page, live usage limits, and /cline slash-command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|