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.
Files changed (49) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README-es.md +35 -5
  3. package/README-hi.md +33 -5
  4. package/README-pt.md +31 -5
  5. package/README.en.md +32 -4
  6. package/README.md +25 -5
  7. package/client.js +117 -17
  8. package/cordis.patch.yml +13 -0
  9. package/lib/config.d.ts +20 -0
  10. package/lib/config.d.ts.map +1 -1
  11. package/lib/config.js +38 -4
  12. package/lib/config.js.map +1 -1
  13. package/lib/content-search-worker.d.ts +8 -0
  14. package/lib/content-search-worker.d.ts.map +1 -0
  15. package/lib/content-search-worker.js +54 -0
  16. package/lib/content-search-worker.js.map +1 -0
  17. package/lib/content-search.d.ts +15 -0
  18. package/lib/content-search.d.ts.map +1 -0
  19. package/lib/content-search.js +38 -0
  20. package/lib/content-search.js.map +1 -0
  21. package/lib/content.d.ts +70 -4
  22. package/lib/content.d.ts.map +1 -1
  23. package/lib/content.js +423 -77
  24. package/lib/content.js.map +1 -1
  25. package/lib/find.d.ts +1 -1
  26. package/lib/find.d.ts.map +1 -1
  27. package/lib/find.js +8 -3
  28. package/lib/find.js.map +1 -1
  29. package/lib/http.d.ts +1 -1
  30. package/lib/http.d.ts.map +1 -1
  31. package/lib/http.js +4 -2
  32. package/lib/http.js.map +1 -1
  33. package/lib/index.d.ts +5 -1
  34. package/lib/index.d.ts.map +1 -1
  35. package/lib/index.js +102 -27
  36. package/lib/index.js.map +1 -1
  37. package/lib/reindex.d.ts +1 -1
  38. package/lib/reindex.d.ts.map +1 -1
  39. package/lib/reindex.js +2 -1
  40. package/lib/reindex.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/config.ts +58 -4
  43. package/src/content-search-worker.ts +52 -0
  44. package/src/content-search.ts +39 -0
  45. package/src/content.ts +391 -71
  46. package/src/find.ts +8 -3
  47. package/src/http.ts +4 -2
  48. package/src/index.ts +102 -25
  49. 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
- return [{ type: 'text', text: `${String(v.hits?.length ?? 0)} content hit(s)${v.state?.building ? ' (index still building)' : ''}\n${lines.join('\n')}` }]
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 three tools.
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 timer = setTimeout(() => {
136
+ const start = (): void => {
54
137
  rt.start()
55
138
  logger.info(`index roots: ${roots.join(', ')} (state ${stateDir})`)
56
- if (content !== undefined) void startContent(rt, content, logger, config.contentRefreshMs)
57
- }, 1500)
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
- clearTimeout(timer)
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
- // three tools and never mount an HTTP route.
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' }