dsh-locator 0.7.0 → 0.7.4
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 +30 -0
- package/README-es.md +35 -5
- package/README-hi.md +33 -5
- package/README-pt.md +31 -5
- package/README.en.md +32 -4
- package/README.md +25 -5
- package/client.js +117 -17
- package/cordis.patch.yml +13 -0
- package/lib/config.d.ts +20 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +38 -4
- package/lib/config.js.map +1 -1
- package/lib/content-search-worker.d.ts +8 -0
- package/lib/content-search-worker.d.ts.map +1 -0
- package/lib/content-search-worker.js +54 -0
- package/lib/content-search-worker.js.map +1 -0
- package/lib/content-search.d.ts +15 -0
- package/lib/content-search.d.ts.map +1 -0
- package/lib/content-search.js +38 -0
- package/lib/content-search.js.map +1 -0
- package/lib/content.d.ts +70 -4
- package/lib/content.d.ts.map +1 -1
- package/lib/content.js +423 -77
- package/lib/content.js.map +1 -1
- package/lib/find.d.ts +1 -1
- package/lib/find.d.ts.map +1 -1
- package/lib/find.js +8 -3
- package/lib/find.js.map +1 -1
- package/lib/http.d.ts +1 -1
- package/lib/http.d.ts.map +1 -1
- package/lib/http.js +4 -2
- package/lib/http.js.map +1 -1
- package/lib/index.d.ts +5 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +102 -27
- package/lib/index.js.map +1 -1
- package/lib/reindex.d.ts +1 -1
- package/lib/reindex.d.ts.map +1 -1
- package/lib/reindex.js +2 -1
- package/lib/reindex.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +58 -4
- package/src/content-search-worker.ts +52 -0
- package/src/content-search.ts +39 -0
- package/src/content.ts +391 -71
- package/src/find.ts +8 -3
- package/src/http.ts +4 -2
- package/src/index.ts +102 -25
- package/src/reindex.ts +2 -1
package/src/find.ts
CHANGED
|
@@ -21,7 +21,7 @@ const HIT = {
|
|
|
21
21
|
} as const
|
|
22
22
|
|
|
23
23
|
/** Build the locator_find tool. */
|
|
24
|
-
export function makeFindTool(content: ContentIndex) {
|
|
24
|
+
export function makeFindTool(content: ContentIndex, ensure?: () => void) {
|
|
25
25
|
return defineTool({
|
|
26
26
|
name: 'locator_find',
|
|
27
27
|
description: [
|
|
@@ -63,14 +63,19 @@ export function makeFindTool(content: ContentIndex) {
|
|
|
63
63
|
const v = value as { ok: boolean; hits?: Array<{ kind: string; title?: string; snippet?: string; path?: string }>; error?: string; state?: ContentState }
|
|
64
64
|
if (!v.ok) return [{ type: 'text', text: `locator_find failed: ${v.error ?? 'unknown'}` }]
|
|
65
65
|
const lines = (v.hits ?? []).map((hit, index) => `${String(index + 1)}. [${hit.kind}] ${hit.title ?? ''} — ${hit.snippet ?? ''}\n ${hit.path ?? ''}`)
|
|
66
|
-
|
|
66
|
+
const notes = [
|
|
67
|
+
v.state?.building ? ' (index still building)' : '',
|
|
68
|
+
v.state?.truncated === true ? ' (content budget reached: raise contentMaxTotalBytes or trim contentExcludes; later files are not searchable)' : '',
|
|
69
|
+
].join('')
|
|
70
|
+
return [{ type: 'text', text: `${String(v.hits?.length ?? 0)} content hit(s)${notes}\n${lines.join('\n')}` }]
|
|
67
71
|
},
|
|
68
72
|
},
|
|
69
73
|
async execute(args) {
|
|
74
|
+
ensure?.()
|
|
70
75
|
const query = args.query.trim()
|
|
71
76
|
if (query === '') return { ok: false as const, error: 'query is empty' }
|
|
72
77
|
const limit = Math.max(1, Math.min(100, args.limit ?? 30))
|
|
73
|
-
const hits = content.search(query, limit)
|
|
78
|
+
const hits = await content.search(query, limit)
|
|
74
79
|
return { ok: true as const, query, hits, state: content.status() }
|
|
75
80
|
},
|
|
76
81
|
})
|
package/src/http.ts
CHANGED
|
@@ -47,7 +47,7 @@ interface SearchBody {
|
|
|
47
47
|
const BODY_LIMIT = 64 * 1024
|
|
48
48
|
|
|
49
49
|
/** Mount the panel's routes when this profile runs a Web server. */
|
|
50
|
-
export function registerHttp(ctx: Context, rt: LocatorRuntime, content?: ContentIndex): void {
|
|
50
|
+
export function registerHttp(ctx: Context, rt: LocatorRuntime, content?: ContentIndex, ensure?: () => void): void {
|
|
51
51
|
const inject = (ctx as unknown as {
|
|
52
52
|
inject?: (deps: string[], callback: (scoped: Context) => void) => void
|
|
53
53
|
}).inject
|
|
@@ -75,6 +75,7 @@ export function registerHttp(ctx: Context, rt: LocatorRuntime, content?: Content
|
|
|
75
75
|
kind: 'exact',
|
|
76
76
|
path: '/dsh-locator/content/rebuild',
|
|
77
77
|
handler: handler(scoped, async body => {
|
|
78
|
+
ensure?.()
|
|
78
79
|
const full = body.full === true
|
|
79
80
|
const started = full ? content.rebuild() : content.refreshNow()
|
|
80
81
|
return started ? { ok: true, started: true, full } : { ok: false, error: 'a content build is already running' }
|
|
@@ -84,9 +85,10 @@ export function registerHttp(ctx: Context, rt: LocatorRuntime, content?: Content
|
|
|
84
85
|
kind: 'exact',
|
|
85
86
|
path: '/dsh-locator/content',
|
|
86
87
|
handler: handler(scoped, async body => {
|
|
88
|
+
ensure?.()
|
|
87
89
|
const query = typeof body.query === 'string' ? body.query : ''
|
|
88
90
|
const limit = typeof body.limit === 'number' ? body.limit : 30
|
|
89
|
-
return { ok: true, query, hits: content.search(query, limit), state: content.status() }
|
|
91
|
+
return { ok: true, query, hits: await content.search(query, limit), state: content.status() }
|
|
90
92
|
}),
|
|
91
93
|
}))
|
|
92
94
|
}
|
package/src/index.ts
CHANGED
|
@@ -24,8 +24,46 @@ export const name = 'dsh-locator'
|
|
|
24
24
|
/** Tools are registered through the host tool registry. */
|
|
25
25
|
export const inject = ['tools']
|
|
26
26
|
|
|
27
|
+
/** The timer service mounted by `@deepseek-ai/cordis-plugin-timer`. */
|
|
28
|
+
interface TimerLike {
|
|
29
|
+
timeout?: (callback: () => void, delay: number) => unknown
|
|
30
|
+
interval?: (callback: () => void, delay: number) => unknown
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Read the optional timer service without declaring it as a hard dependency.
|
|
35
|
+
* Cordis refuses direct mixin access (`ctx.timeout`) unless the plugin injects
|
|
36
|
+
* `timer`, but `ctx.get()` is the documented escape hatch for optional
|
|
37
|
+
* services; a profile without the timer plugin simply gets the raw fallback.
|
|
38
|
+
* @param ctx - the plugin context.
|
|
39
|
+
* @returns the timer service, or undefined when the profile has none.
|
|
40
|
+
*/
|
|
41
|
+
function optionalTimer(ctx: Context): TimerLike | undefined {
|
|
42
|
+
const get = (ctx as unknown as { get?: (name: string, strict?: boolean) => unknown }).get
|
|
43
|
+
if (typeof get !== 'function') return undefined
|
|
44
|
+
try {
|
|
45
|
+
const timer = get.call(ctx, 'timer', false) as TimerLike | undefined
|
|
46
|
+
return timer ?? undefined
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Normalize a cordis timer handle (function or `.dispose()`) to a disposer. */
|
|
53
|
+
function asDisposer(handle: unknown): (() => void) | undefined {
|
|
54
|
+
if (typeof handle === 'function') return handle as () => void
|
|
55
|
+
if (handle !== null && typeof handle === 'object' && typeof (handle as { dispose?: unknown }).dispose === 'function') {
|
|
56
|
+
return () => { (handle as { dispose: () => void }).dispose() }
|
|
57
|
+
}
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
27
61
|
/**
|
|
28
|
-
* Mount the locator engine and its
|
|
62
|
+
* Mount the locator engine and its tools.
|
|
63
|
+
*
|
|
64
|
+
* The name index starts right away; the content index is built lazily on the
|
|
65
|
+
* first content search (or eagerly when `contentLazy` is false) because it is
|
|
66
|
+
* the expensive half and most sessions never search bodies.
|
|
29
67
|
* @param ctx - the plugin context.
|
|
30
68
|
* @param config - validated configuration.
|
|
31
69
|
*/
|
|
@@ -41,22 +79,77 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
41
79
|
? new ContentIndex(rt, {
|
|
42
80
|
stateDir,
|
|
43
81
|
sessionDirs: config.contentSessionDirs.length > 0 ? config.contentSessionDirs : undefined,
|
|
82
|
+
excludes: config.contentExcludes,
|
|
83
|
+
includeArchived: config.contentIncludeArchived,
|
|
84
|
+
activeSessionDelayMs: config.contentActiveSessionDelayMs,
|
|
85
|
+
searchWorker: true,
|
|
44
86
|
maxFileBytes: config.contentMaxFileBytes,
|
|
45
87
|
maxTotalBytes: config.contentMaxTotalBytes,
|
|
46
88
|
maxFiles: config.contentMaxFiles,
|
|
47
89
|
maxSessions: config.contentMaxSessions,
|
|
48
90
|
})
|
|
49
91
|
: undefined
|
|
92
|
+
const timer = optionalTimer(ctx)
|
|
93
|
+
let contentStarted = false
|
|
94
|
+
let refreshHandle: (() => void) | undefined
|
|
95
|
+
|
|
96
|
+
const armRefresh = (): void => {
|
|
97
|
+
if (content === undefined || config.contentRefreshMs <= 0 || refreshHandle !== undefined) return
|
|
98
|
+
const tick = (): void => { content.refreshNow() }
|
|
99
|
+
if (typeof timer?.interval === 'function') {
|
|
100
|
+
const handle = asDisposer(timer.interval(tick, config.contentRefreshMs))
|
|
101
|
+
if (handle !== undefined) { refreshHandle = handle; return }
|
|
102
|
+
}
|
|
103
|
+
const handle = setInterval(tick, config.contentRefreshMs)
|
|
104
|
+
refreshHandle = () => { clearInterval(handle) }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Start the content half exactly once. Tools and the panel call this on
|
|
109
|
+
* their first content operation so that a profile that never searches
|
|
110
|
+
* bodies pays nothing for the content database.
|
|
111
|
+
*/
|
|
112
|
+
const ensureContent = (): void => {
|
|
113
|
+
if (content === undefined || contentStarted) return
|
|
114
|
+
contentStarted = true
|
|
115
|
+
void startContent()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const startContent = async (): Promise<void> => {
|
|
119
|
+
try {
|
|
120
|
+
await rt.waitReady()
|
|
121
|
+
for (let i = 0; i < 900; i++) {
|
|
122
|
+
const status = await rt.engineStatus()
|
|
123
|
+
if (status === undefined || !status.scanning) break
|
|
124
|
+
await new Promise(resolve => setTimeout(resolve, 2_000))
|
|
125
|
+
}
|
|
126
|
+
content!.boot()
|
|
127
|
+
armRefresh()
|
|
128
|
+
logger.info('content index started')
|
|
129
|
+
} catch (error) {
|
|
130
|
+
logger.info(`content index skipped: ${error instanceof Error ? error.message : String(error)}`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
50
133
|
|
|
51
134
|
ctx.effect(() => {
|
|
52
135
|
// Let the host finish starting before a cold scan starts reading the disk.
|
|
53
|
-
const
|
|
136
|
+
const start = (): void => {
|
|
54
137
|
rt.start()
|
|
55
138
|
logger.info(`index roots: ${roots.join(', ')} (state ${stateDir})`)
|
|
56
|
-
if (content !== undefined
|
|
57
|
-
}
|
|
139
|
+
if (content !== undefined && config.contentLazy === false) ensureContent()
|
|
140
|
+
}
|
|
141
|
+
let disposeBoot: (() => void) | undefined
|
|
142
|
+
if (typeof timer?.timeout === 'function') {
|
|
143
|
+
disposeBoot = asDisposer(timer.timeout(start, 1500))
|
|
144
|
+
}
|
|
145
|
+
if (disposeBoot === undefined) {
|
|
146
|
+
const handle = setTimeout(start, 1500)
|
|
147
|
+
disposeBoot = () => { clearTimeout(handle) }
|
|
148
|
+
}
|
|
58
149
|
return () => {
|
|
59
|
-
|
|
150
|
+
disposeBoot?.()
|
|
151
|
+
refreshHandle?.()
|
|
152
|
+
refreshHandle = undefined
|
|
60
153
|
content?.close()
|
|
61
154
|
void rt.stop()
|
|
62
155
|
}
|
|
@@ -67,27 +160,11 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
67
160
|
ctx.tools.register(makeRebuildTool(rt))
|
|
68
161
|
ctx.tools.register(makeExportTool(rt))
|
|
69
162
|
if (content !== undefined) {
|
|
70
|
-
ctx.tools.register(makeFindTool(content))
|
|
71
|
-
ctx.tools.register(makeReindexTool(content))
|
|
163
|
+
ctx.tools.register(makeFindTool(content, ensureContent))
|
|
164
|
+
ctx.tools.register(makeReindexTool(content, ensureContent))
|
|
72
165
|
}
|
|
73
166
|
|
|
74
167
|
// The Web GUI panel is optional: profiles without a Web server keep the
|
|
75
|
-
//
|
|
76
|
-
registerHttp(ctx, rt, content)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function startContent(rt: LocatorRuntime, content: ContentIndex, logger: { info(message: string): void }, refreshMs: number): Promise<void> {
|
|
80
|
-
try {
|
|
81
|
-
await rt.waitReady()
|
|
82
|
-
for (let i = 0; i < 900; i++) {
|
|
83
|
-
const status = await rt.engineStatus()
|
|
84
|
-
if (status === undefined || !status.scanning) break
|
|
85
|
-
await new Promise(resolve => setTimeout(resolve, 2_000))
|
|
86
|
-
}
|
|
87
|
-
content.start()
|
|
88
|
-
content.startAutoRefresh(refreshMs)
|
|
89
|
-
logger.info('content index started')
|
|
90
|
-
} catch (error) {
|
|
91
|
-
logger.info(`content index skipped: ${error instanceof Error ? error.message : String(error)}`)
|
|
92
|
-
}
|
|
168
|
+
// tools and never mount an HTTP route.
|
|
169
|
+
registerHttp(ctx, rt, content, ensureContent)
|
|
93
170
|
}
|
package/src/reindex.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
3
3
|
import type { ContentIndex } from './content.ts'
|
|
4
4
|
|
|
5
|
-
export function makeReindexTool(content: ContentIndex) {
|
|
5
|
+
export function makeReindexTool(content: ContentIndex, ensure?: () => void) {
|
|
6
6
|
return defineTool({
|
|
7
7
|
name: 'locator_reindex',
|
|
8
8
|
description: [
|
|
@@ -30,6 +30,7 @@ export function makeReindexTool(content: ContentIndex) {
|
|
|
30
30
|
},
|
|
31
31
|
},
|
|
32
32
|
async execute(args) {
|
|
33
|
+
ensure?.()
|
|
33
34
|
const full = args.full === true
|
|
34
35
|
const started = full ? content.rebuild() : content.refreshNow()
|
|
35
36
|
if (!started) return { ok: false as const, error: 'a content build is already running' }
|