dsh-locator 0.8.2 → 0.9.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/CHANGELOG.md +13 -0
- package/README-es.md +21 -5
- package/README-hi.md +22 -5
- package/README-pt.md +22 -5
- package/README.en.md +22 -5
- package/README.md +22 -5
- package/client.js +931 -41
- package/lib/combined.d.ts +20 -3
- package/lib/combined.d.ts.map +1 -1
- package/lib/combined.js +72 -12
- package/lib/combined.js.map +1 -1
- package/lib/content-search-worker.d.ts +3 -3
- package/lib/content-search-worker.js +11 -6
- package/lib/content-search-worker.js.map +1 -1
- package/lib/content-search.d.ts +81 -7
- package/lib/content-search.d.ts.map +1 -1
- package/lib/content-search.js +454 -36
- package/lib/content-search.js.map +1 -1
- package/lib/content.d.ts +20 -0
- package/lib/content.d.ts.map +1 -1
- package/lib/content.js +71 -21
- package/lib/content.js.map +1 -1
- package/lib/find.d.ts.map +1 -1
- package/lib/find.js +2 -1
- package/lib/find.js.map +1 -1
- package/lib/http.d.ts.map +1 -1
- package/lib/http.js +9 -2
- package/lib/http.js.map +1 -1
- package/package.json +1 -1
- package/src/combined.ts +95 -14
- package/src/content-search-worker.ts +15 -8
- package/src/content-search.ts +559 -36
- package/src/content.ts +81 -18
- package/src/find.ts +3 -2
- package/src/http.ts +11 -2
package/src/content.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { DatabaseSync } from 'node:sqlite'
|
|
|
27
27
|
import { randomUUID } from 'node:crypto'
|
|
28
28
|
import { fileURLToPath } from 'node:url'
|
|
29
29
|
import { Worker } from 'node:worker_threads'
|
|
30
|
-
import { searchContent } from './content-search.ts'
|
|
30
|
+
import { searchContent, type ContentSearchOptions, type ContentSearchResult } from './content-search.ts'
|
|
31
31
|
import type { LocatorRuntime } from './runtime.ts'
|
|
32
32
|
|
|
33
33
|
const SCHEMA_VERSION = 3
|
|
@@ -67,6 +67,8 @@ export interface ContentHit {
|
|
|
67
67
|
messageId?: string
|
|
68
68
|
/** Exact `data-chat-node-key` of the rendered row, when the host has one. */
|
|
69
69
|
nodeKey?: string
|
|
70
|
+
/** 1-based line of the first match inside the stored body, for file hits. */
|
|
71
|
+
line?: number
|
|
70
72
|
score: number
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -128,7 +130,9 @@ export class ContentIndex {
|
|
|
128
130
|
private workerSeq = 0
|
|
129
131
|
private pendingStart = false
|
|
130
132
|
private dbIdentity: { dev: number; ino: number } | null = null
|
|
131
|
-
|
|
133
|
+
/** Session id → indexed session log path, for session-scoped content search. */
|
|
134
|
+
private readonly sessionPaths = new Map<string, string>()
|
|
135
|
+
private readonly pending = new Map<number, { resolve: (result: ContentSearchResult) => void; reject: (error: Error) => void }>()
|
|
132
136
|
|
|
133
137
|
constructor(private readonly rt: LocatorRuntime, private readonly options: ContentOptions) {}
|
|
134
138
|
|
|
@@ -176,6 +180,7 @@ export class ContentIndex {
|
|
|
176
180
|
this.releaseLock()
|
|
177
181
|
this.pendingStart = false
|
|
178
182
|
this.dbIdentity = null
|
|
183
|
+
this.sessionPaths.clear()
|
|
179
184
|
}
|
|
180
185
|
|
|
181
186
|
/** Build everything from scratch. */
|
|
@@ -347,6 +352,7 @@ export class ContentIndex {
|
|
|
347
352
|
this.db.exec("INSERT OR REPLACE INTO content_meta (key, value) VALUES ('complete', 0)")
|
|
348
353
|
}
|
|
349
354
|
if (fresh) this.db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`)
|
|
355
|
+
this.refreshSessionPaths()
|
|
350
356
|
this.reopenWorker()
|
|
351
357
|
}
|
|
352
358
|
|
|
@@ -358,7 +364,7 @@ export class ContentIndex {
|
|
|
358
364
|
private stopWorker(): void {
|
|
359
365
|
const worker = this.worker
|
|
360
366
|
this.worker = null
|
|
361
|
-
for (const entry of this.pending.values()) entry.resolve(
|
|
367
|
+
for (const entry of this.pending.values()) entry.resolve(emptySearchResult())
|
|
362
368
|
this.pending.clear()
|
|
363
369
|
if (worker !== null) void worker.terminate()
|
|
364
370
|
}
|
|
@@ -373,11 +379,15 @@ export class ContentIndex {
|
|
|
373
379
|
const worker = new Worker(workerPath, {
|
|
374
380
|
workerData: { dbPath: join(this.options.stateDir, 'content.db') },
|
|
375
381
|
})
|
|
376
|
-
worker.on('message', (message: { id: number;
|
|
382
|
+
worker.on('message', (message: { id: number; result?: ContentSearchResult; error?: string }) => {
|
|
377
383
|
const entry = this.pending.get(message.id)
|
|
378
384
|
if (entry === undefined) return
|
|
379
385
|
this.pending.delete(message.id)
|
|
380
|
-
|
|
386
|
+
if (typeof message.error === 'string' && message.error !== '') {
|
|
387
|
+
entry.reject(new Error(message.error))
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
entry.resolve(message.result ?? emptySearchResult())
|
|
381
391
|
})
|
|
382
392
|
const fail = (): void => {
|
|
383
393
|
if (this.worker !== worker) return
|
|
@@ -424,6 +434,7 @@ export class ContentIndex {
|
|
|
424
434
|
this.dbIdentity = null
|
|
425
435
|
this.stopWorker()
|
|
426
436
|
this.totalBytes = 0
|
|
437
|
+
this.sessionPaths.clear()
|
|
427
438
|
this.state.updatedAt = 0
|
|
428
439
|
this.state.error = ''
|
|
429
440
|
this.state.truncated = false
|
|
@@ -502,9 +513,32 @@ export class ContentIndex {
|
|
|
502
513
|
this.state.updatedAt = Date.now()
|
|
503
514
|
this.writeTotalBytes()
|
|
504
515
|
this.writeFlag('complete', 1)
|
|
516
|
+
this.refreshSessionPaths()
|
|
505
517
|
this.reopenWorker()
|
|
506
518
|
}
|
|
507
519
|
|
|
520
|
+
/**
|
|
521
|
+
* Rebuild the session id → session log path map from the persisted sources.
|
|
522
|
+
* Runs after `open()` and after every build/refresh, so session-scoped
|
|
523
|
+
* searches work against an existing database before the first refresh ends.
|
|
524
|
+
*/
|
|
525
|
+
private refreshSessionPaths(): void {
|
|
526
|
+
this.sessionPaths.clear()
|
|
527
|
+
if (this.db === null) return
|
|
528
|
+
try {
|
|
529
|
+
const rows = this.db.prepare('SELECT path FROM sources').all() as Array<{ path?: string }>
|
|
530
|
+
for (const row of rows) {
|
|
531
|
+
const path = row.path
|
|
532
|
+
if (typeof path !== 'string' || path === '') continue
|
|
533
|
+
const sessionId = basename(dirname(path))
|
|
534
|
+
if (sessionId !== '') this.sessionPaths.set(sessionId, path)
|
|
535
|
+
}
|
|
536
|
+
} catch {
|
|
537
|
+
// A database from an older schema cannot answer session-scoped queries
|
|
538
|
+
// until its next rebuild; leave the map empty instead of failing boot.
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
508
542
|
private addDoc(row: DocRow): boolean {
|
|
509
543
|
const cap = this.options.maxTotalBytes ?? 0
|
|
510
544
|
if (cap > 0 && this.totalBytes + row.body.length > cap) {
|
|
@@ -669,6 +703,7 @@ export class ContentIndex {
|
|
|
669
703
|
try { text = (await zstdDecompressAsync(await readFile(file.path))).toString('utf8') } catch { return true }
|
|
670
704
|
if (generation !== this.generation) return false
|
|
671
705
|
const sessionId = basename(dirname(file.path))
|
|
706
|
+
this.sessionPaths.set(sessionId, file.path)
|
|
672
707
|
let title = ''
|
|
673
708
|
let parsed = 0
|
|
674
709
|
for (const line of text.split('\n')) {
|
|
@@ -712,47 +747,75 @@ export class ContentIndex {
|
|
|
712
747
|
* Search file bodies and session messages. With `searchWorker` enabled the
|
|
713
748
|
* query itself runs off the host thread; a failed or wedged worker falls back
|
|
714
749
|
* to the in-process query so a result is still produced.
|
|
750
|
+
*
|
|
751
|
+
* The two-argument form returns the hit array (tools and the legacy
|
|
752
|
+
* `/content` route). The three-argument form returns hits plus count,
|
|
753
|
+
* warnings and an error code, and understands the advanced match options.
|
|
715
754
|
* @param query - words or a phrase to find.
|
|
716
755
|
* @param limit - maximum hits.
|
|
756
|
+
* @param options - match mode, case/whole-word flags, session scope, count mode.
|
|
717
757
|
* @returns matching documents ordered by relevance.
|
|
718
758
|
*/
|
|
719
|
-
async search(query: string, limit: number): Promise<ContentHit[]>
|
|
720
|
-
|
|
759
|
+
async search(query: string, limit: number): Promise<ContentHit[]>
|
|
760
|
+
async search(query: string, limit: number, options: ContentSearchOptions): Promise<ContentSearchResult>
|
|
761
|
+
async search(query: string, limit: number, options?: ContentSearchOptions): Promise<ContentHit[] | ContentSearchResult> {
|
|
762
|
+
const result = await this.searchDetailed(query, limit, options ?? {})
|
|
763
|
+
return options === undefined ? result.hits : result
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
private async searchDetailed(query: string, limit: number, options: ContentSearchOptions): Promise<ContentSearchResult> {
|
|
767
|
+
const resolved = this.resolveSession(options)
|
|
768
|
+
if (resolved === null) return emptySearchResult(['unknown-session'])
|
|
769
|
+
if (this.db === null) return emptySearchResult()
|
|
721
770
|
if (!this.databaseIsCurrent()) {
|
|
722
771
|
this.recoverExternal()
|
|
723
|
-
return
|
|
772
|
+
return emptySearchResult()
|
|
724
773
|
}
|
|
725
|
-
if (this.options.searchWorker !== true) return this.searchSync(query, limit)
|
|
774
|
+
if (this.options.searchWorker !== true) return this.searchSync(query, limit, resolved)
|
|
726
775
|
this.ensureWorker()
|
|
727
|
-
if (this.worker === null) return this.searchSync(query, limit)
|
|
776
|
+
if (this.worker === null) return this.searchSync(query, limit, resolved)
|
|
728
777
|
const id = ++this.workerSeq
|
|
729
778
|
const worker = this.worker
|
|
730
|
-
const response = new Promise<
|
|
731
|
-
worker.postMessage({ id, query, limit })
|
|
779
|
+
const response = new Promise<ContentSearchResult>((resolve, reject) => { this.pending.set(id, { resolve, reject }) })
|
|
780
|
+
worker.postMessage({ id, query, limit, options: resolved })
|
|
732
781
|
let timer: NodeJS.Timeout | undefined
|
|
733
|
-
const timeout = new Promise<
|
|
782
|
+
const timeout = new Promise<ContentSearchResult>((_resolve, reject) => {
|
|
734
783
|
timer = setTimeout(() => { this.pending.delete(id); reject(new Error('content search worker timed out')) }, 5_000)
|
|
735
784
|
})
|
|
736
785
|
try {
|
|
737
786
|
return await Promise.race([response, timeout])
|
|
738
787
|
} catch {
|
|
739
|
-
return this.searchSync(query, limit)
|
|
788
|
+
return this.searchSync(query, limit, resolved)
|
|
740
789
|
} finally {
|
|
741
790
|
if (timer !== undefined) clearTimeout(timer)
|
|
742
791
|
}
|
|
743
792
|
}
|
|
744
793
|
|
|
745
|
-
|
|
746
|
-
|
|
794
|
+
/** Map a request's sessionId onto the indexed session log path. */
|
|
795
|
+
private resolveSession(options: ContentSearchOptions): ContentSearchOptions | null {
|
|
796
|
+
if (options.sessionPath !== undefined && options.sessionPath !== null) return options
|
|
797
|
+
if (options.sessionId === undefined || options.sessionId === '') return { ...options, sessionPath: null }
|
|
798
|
+
const path = this.sessionPaths.get(options.sessionId)
|
|
799
|
+
if (path === undefined) return null
|
|
800
|
+
return { ...options, sessionPath: path }
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private searchSync(query: string, limit: number, options: ContentSearchOptions): ContentSearchResult {
|
|
804
|
+
if (this.db === null) return emptySearchResult()
|
|
747
805
|
try {
|
|
748
|
-
return searchContent(this.db, query, limit)
|
|
806
|
+
return searchContent(this.db, query, limit, options)
|
|
749
807
|
} catch (error) {
|
|
750
808
|
this.state.error = messageOf(error)
|
|
751
|
-
return
|
|
809
|
+
return emptySearchResult()
|
|
752
810
|
}
|
|
753
811
|
}
|
|
754
812
|
}
|
|
755
813
|
|
|
814
|
+
/** Result used when a content query could not run at all. */
|
|
815
|
+
function emptySearchResult(warnings: string[] = []): ContentSearchResult {
|
|
816
|
+
return { hits: [], count: { value: 0, kind: 'unknown' }, warnings }
|
|
817
|
+
}
|
|
818
|
+
|
|
756
819
|
function dshHome(): string {
|
|
757
820
|
return process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
758
821
|
}
|
package/src/find.ts
CHANGED
|
@@ -17,6 +17,7 @@ const HIT = {
|
|
|
17
17
|
sessionId: { type: 'string' },
|
|
18
18
|
messageId: { type: 'string' },
|
|
19
19
|
nodeKey: { type: 'string' },
|
|
20
|
+
line: { type: 'integer', description: '1-based line of the first match in the stored body (file hits).' },
|
|
20
21
|
score: { type: 'number' },
|
|
21
22
|
},
|
|
22
23
|
} as const
|
|
@@ -61,9 +62,9 @@ export function makeFindTool(content: ContentIndex, ensure?: () => void) {
|
|
|
61
62
|
},
|
|
62
63
|
},
|
|
63
64
|
render: (_args, value) => {
|
|
64
|
-
const v = value as { ok: boolean; hits?: Array<{ kind: string; title?: string; snippet?: string; path?: string }>; error?: string; state?: ContentState }
|
|
65
|
+
const v = value as { ok: boolean; hits?: Array<{ kind: string; title?: string; snippet?: string; path?: string; line?: number }>; error?: string; state?: ContentState }
|
|
65
66
|
if (!v.ok) return [{ type: 'text', text: `locator_find failed: ${v.error ?? 'unknown'}` }]
|
|
66
|
-
const lines = (v.hits ?? []).map((hit, index) => `${String(index + 1)}. [${hit.kind}] ${hit.title ?? ''} — ${hit.snippet ?? ''}\n ${hit.path ?? ''}`)
|
|
67
|
+
const lines = (v.hits ?? []).map((hit, index) => `${String(index + 1)}. [${hit.kind}] ${hit.title ?? ''} — ${hit.snippet ?? ''}\n ${hit.path ?? ''}${typeof hit.line === 'number' ? ` (line ${String(hit.line)})` : ''}`)
|
|
67
68
|
const notes = [
|
|
68
69
|
v.state?.building ? ' (index still building)' : '',
|
|
69
70
|
v.state?.truncated === true ? ' (content budget reached: raise contentMaxTotalBytes or trim contentExcludes; later files are not searchable)' : '',
|
package/src/http.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { Context } from '@deepseek-ai/cordis'
|
|
|
16
16
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
17
17
|
import type { LocatorRuntime, SearchArgs } from './runtime.ts'
|
|
18
18
|
import type { ContentIndex } from './content.ts'
|
|
19
|
+
import type { ContentSearchOptions } from './content-search.ts'
|
|
19
20
|
import { readCombinedRequest, runCombinedSearch, type CombinedDeps } from './combined.ts'
|
|
20
21
|
import { openWithDefaultApp, revealInFileManager, withinRoots } from './reveal.ts'
|
|
21
22
|
import { statSync } from 'node:fs'
|
|
@@ -176,14 +177,22 @@ async function combined(
|
|
|
176
177
|
search: async args => await rt.search(args),
|
|
177
178
|
maxResults: rt.config.maxResults,
|
|
178
179
|
...(content === undefined ? {} : {
|
|
179
|
-
contentSearch: async (query: string, limit: number) =>
|
|
180
|
+
contentSearch: async (query: string, limit: number, options: ContentSearchOptions) =>
|
|
181
|
+
await content.search(query, limit, options),
|
|
180
182
|
contentStatus: () => content.status(),
|
|
181
183
|
...(ensure === undefined ? {} : { ensure }),
|
|
182
184
|
}),
|
|
183
185
|
}
|
|
184
186
|
const request = readCombinedRequest(raw, rt.config.maxResults)
|
|
185
187
|
const result = await runCombinedSearch(deps, request)
|
|
186
|
-
return {
|
|
188
|
+
return {
|
|
189
|
+
query: request.query,
|
|
190
|
+
matchMode: request.matchMode,
|
|
191
|
+
caseSensitive: request.caseSensitive,
|
|
192
|
+
wholeWord: request.wholeWord,
|
|
193
|
+
sessionId: request.sessionId,
|
|
194
|
+
...result,
|
|
195
|
+
}
|
|
187
196
|
}
|
|
188
197
|
|
|
189
198
|
async function reveal(rt: LocatorRuntime, raw: Record<string, unknown>): Promise<unknown> {
|