dsh-locator 0.6.2 → 0.7.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,MAAM,IAAI,YAAY,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AASzC,OAAO,EAAE,YAAY,IAAI,MAAM,EAAE,CAAA;AAEjC,0BAA0B;AAC1B,eAAO,MAAM,IAAI,gBAAgB,CAAA;AAEjC,2DAA2D;AAC3D,eAAO,MAAM,MAAM,UAAY,CAAA;AAE/B;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAyCxD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,MAAM,IAAI,YAAY,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAUzC,OAAO,EAAE,YAAY,IAAI,MAAM,EAAE,CAAA;AAEjC,0BAA0B;AAC1B,eAAO,MAAM,IAAI,gBAAgB,CAAA;AAEjC,2DAA2D;AAC3D,eAAO,MAAM,MAAM,UAAY,CAAA;AAE/B;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CA6CxD"}
package/lib/index.js CHANGED
@@ -7,6 +7,7 @@ import { Config as ConfigSchema } from "./config.js";
7
7
  import { ContentIndex } from "./content.js";
8
8
  import { makeExportTool } from "./export.js";
9
9
  import { makeFindTool } from "./find.js";
10
+ import { makeReindexTool } from "./reindex.js";
10
11
  import { registerHttp } from "./http.js";
11
12
  import { resolveBinary, resolveRoots, resolveStateDir } from "./paths.js";
12
13
  import { LocatorRuntime } from "./runtime.js";
@@ -33,8 +34,9 @@ export function apply(ctx, config) {
33
34
  const content = config.contentIndex
34
35
  ? new ContentIndex(rt, {
35
36
  stateDir,
36
- sessionDirs: config.contentSessionDirs,
37
+ sessionDirs: config.contentSessionDirs.length > 0 ? config.contentSessionDirs : undefined,
37
38
  maxFileBytes: config.contentMaxFileBytes,
39
+ maxTotalBytes: config.contentMaxTotalBytes,
38
40
  maxFiles: config.contentMaxFiles,
39
41
  maxSessions: config.contentMaxSessions,
40
42
  })
@@ -45,7 +47,7 @@ export function apply(ctx, config) {
45
47
  rt.start();
46
48
  logger.info(`index roots: ${roots.join(', ')} (state ${stateDir})`);
47
49
  if (content !== undefined)
48
- void startContent(rt, content, logger);
50
+ void startContent(rt, content, logger, config.contentRefreshMs);
49
51
  }, 1500);
50
52
  return () => {
51
53
  clearTimeout(timer);
@@ -57,13 +59,15 @@ export function apply(ctx, config) {
57
59
  ctx.tools.register(makeStatusTool(rt));
58
60
  ctx.tools.register(makeRebuildTool(rt));
59
61
  ctx.tools.register(makeExportTool(rt));
60
- if (content !== undefined)
62
+ if (content !== undefined) {
61
63
  ctx.tools.register(makeFindTool(content));
64
+ ctx.tools.register(makeReindexTool(content));
65
+ }
62
66
  // The Web GUI panel is optional: profiles without a Web server keep the
63
67
  // three tools and never mount an HTTP route.
64
68
  registerHttp(ctx, rt, content);
65
69
  }
66
- async function startContent(rt, content, logger) {
70
+ async function startContent(rt, content, logger, refreshMs) {
67
71
  try {
68
72
  await rt.waitReady();
69
73
  for (let i = 0; i < 900; i++) {
@@ -73,6 +77,7 @@ async function startContent(rt, content, logger) {
73
77
  await new Promise(resolve => setTimeout(resolve, 2_000));
74
78
  }
75
79
  content.start();
80
+ content.startAutoRefresh(refreshMs);
76
81
  logger.info('content index started');
77
82
  }
78
83
  catch (error) {
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { ContentIndex } from './content.ts';
2
+ export declare function makeReindexTool(content: ContentIndex): import("@deepseek-ai/dsh-tools").ToolDefinition;
3
+ //# sourceMappingURL=reindex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reindex.d.ts","sourceRoot":"","sources":["../src/reindex.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEhD,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,mDAkCpD"}
package/lib/reindex.js ADDED
@@ -0,0 +1,39 @@
1
+ /** locator_reindex — full or incremental content-index maintenance. */
2
+ import { defineTool } from '@deepseek-ai/dsh-tools';
3
+ export function makeReindexTool(content) {
4
+ return defineTool({
5
+ name: 'locator_reindex',
6
+ description: [
7
+ 'Rebuild or refresh the dsh-locator content index (file bodies + DSH session messages).',
8
+ '',
9
+ '`full: true` drops the content database and re-reads everything; `full: false` (default) only re-reads files/sessions whose mtime changed. Name/path search keeps working during either mode. Use `locator_status` or the content state in `locator_find` to watch progress.',
10
+ ].join('\n'),
11
+ parameters: {
12
+ full: { type: 'boolean', description: 'Drop and rebuild the content database (default false = incremental).' },
13
+ },
14
+ output: {
15
+ schema: {
16
+ type: 'object',
17
+ additionalProperties: false,
18
+ properties: {
19
+ ok: { type: 'boolean', required: true },
20
+ started: { type: 'boolean' },
21
+ full: { type: 'boolean' },
22
+ error: { type: 'string' },
23
+ },
24
+ },
25
+ render: (_args, value) => {
26
+ const v = value;
27
+ return [{ type: 'text', text: v.ok ? `${v.full ? 'Full' : 'Incremental'} content rebuild started.` : `locator_reindex failed: ${v.error ?? 'unknown'}` }];
28
+ },
29
+ },
30
+ async execute(args) {
31
+ const full = args.full === true;
32
+ const started = full ? content.rebuild() : content.refreshNow();
33
+ if (!started)
34
+ return { ok: false, error: 'a content build is already running' };
35
+ return { ok: true, started: true, full };
36
+ },
37
+ });
38
+ }
39
+ //# sourceMappingURL=reindex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reindex.js","sourceRoot":"","sources":["../src/reindex.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAGnD,MAAM,UAAU,eAAe,CAAC,OAAqB;IACnD,OAAO,UAAU,CAAC;QAChB,IAAI,EAAE,iBAAiB;QACvB,WAAW,EAAE;YACX,wFAAwF;YACxF,EAAE;YACF,8QAA8Q;SAC/Q,CAAC,IAAI,CAAC,IAAI,CAAC;QACZ,UAAU,EAAE;YACV,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,sEAAsE,EAAE;SAC/G;QACD,MAAM,EAAE;YACN,MAAM,EAAE;gBACN,IAAI,EAAE,QAAQ;gBACd,oBAAoB,EAAE,KAAK;gBAC3B,UAAU,EAAE;oBACV,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE;oBACvC,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;oBAC5B,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;oBACzB,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;iBAC1B;aACF;YACD,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;gBACvB,MAAM,CAAC,GAAG,KAA2E,CAAA;gBACrF,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa,2BAA2B,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,KAAK,IAAI,SAAS,EAAE,EAAE,CAAC,CAAA;YAC3J,CAAC;SACF;QACD,KAAK,CAAC,OAAO,CAAC,IAAI;YAChB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,IAAI,CAAA;YAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,CAAA;YAC/D,IAAI,CAAC,OAAO;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAc,EAAE,KAAK,EAAE,oCAAoC,EAAE,CAAA;YACxF,OAAO,EAAE,EAAE,EAAE,IAAa,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;QACnD,CAAC;KACF,CAAC,CAAA;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-locator",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Whole-disk, millisecond file search for DeepSeek Harness without Everything: a self-built cross-platform index, an Everything-style query language, agent tools and a Web search panel (Ctrl+Shift+F). Windows, macOS and Linux.",
5
5
  "author": "PerryLink contributors",
6
6
  "repository": {
package/src/config.ts CHANGED
@@ -29,6 +29,10 @@ export interface Config {
29
29
  contentIndex: boolean
30
30
  /** Maximum bytes read from one text file. */
31
31
  contentMaxFileBytes: number
32
+ /** Stop adding content after this many body bytes; 0 means unlimited. */
33
+ contentMaxTotalBytes: number
34
+ /** Incremental content refresh cadence; 0 disables it. */
35
+ contentRefreshMs: number
32
36
  /** Maximum text files indexed per build; 0 means no cap. */
33
37
  contentMaxFiles: number
34
38
  /** Most recent session logs indexed; 0 means all. */
@@ -72,9 +76,11 @@ export const Config: Schema<Config> = Schema.object({
72
76
  scanWorkers: Schema.number().default(0),
73
77
  memoryLimitMB: Schema.number().default(0),
74
78
  contentIndex: Schema.boolean().default(true),
75
- contentMaxFileBytes: Schema.number().default(1_000_000),
76
- contentMaxFiles: Schema.number().default(50_000),
77
- contentMaxSessions: Schema.number().default(50),
79
+ contentMaxFileBytes: Schema.number().default(4_000_000),
80
+ contentMaxTotalBytes: Schema.number().default(2_000_000_000),
81
+ contentRefreshMs: Schema.number().default(120_000),
82
+ contentMaxFiles: Schema.number().default(200_000),
83
+ contentMaxSessions: Schema.number().default(200),
78
84
  contentSessionDirs: Schema.array(Schema.string()).default([]),
79
85
  windowsUsn: Schema.boolean().default(false),
80
86
  stateDir: Schema.string().default(''),
package/src/content.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Content search: text files and DSH session messages in SQLite FTS5.
3
3
  *
4
- * The file-name index stays in the Go engine. This store is built beside it:
5
- * the engine's `dump` RPC supplies the already-scanned file list, Node reads
6
- * text files and decompresses `session.v4.jsonl.zstd` logs, and SQLite's
7
- * trigram tokenizer gives substring/CJK search with snippets.
4
+ * File names stay in the Go engine. This store is built from the engine's
5
+ * `dump` RPC plus the session logs: Node reads text bodies, decompresses
6
+ * `session.v4.jsonl.zstd`, and SQLite's trigram tokenizer gives substring and
7
+ * CJK search. A meta table stores each source's mtime so refresh() only
8
+ * re-reads what changed; rebuild() drops and rebuilds everything.
8
9
  * @module dsh-locator/content
9
10
  */
10
11
 
@@ -12,6 +13,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node
12
13
  import { homedir } from 'node:os'
13
14
  import { basename, dirname, extname, join } from 'node:path'
14
15
  import { zstdDecompressSync } from 'node:zlib'
16
+ import { DatabaseSync } from 'node:sqlite'
15
17
  import type { LocatorRuntime } from './runtime.ts'
16
18
 
17
19
  const TEXT_EXTENSIONS = new Set([
@@ -23,7 +25,6 @@ const TEXT_EXTENSIONS = new Set([
23
25
  '.sql', '.graphql', '.proto', '.tex', '.org', '.adoc',
24
26
  ])
25
27
 
26
- /** One searchable document. */
27
28
  export interface ContentHit {
28
29
  kind: 'file' | 'session'
29
30
  path: string
@@ -34,126 +35,173 @@ export interface ContentHit {
34
35
  score: number
35
36
  }
36
37
 
37
- /** Build progress shown by the panel/status. */
38
38
  export interface ContentState {
39
39
  building: boolean
40
40
  files: number
41
41
  sessions: number
42
42
  messages: number
43
+ truncated: boolean
43
44
  error: string
44
45
  updatedAt: number
45
46
  }
46
47
 
47
- /** The content store; one instance per plugin. */
48
+ export interface ContentOptions {
49
+ stateDir: string
50
+ sessionDirs?: string[] | undefined
51
+ maxFileBytes: number
52
+ maxTotalBytes?: number
53
+ maxFiles: number
54
+ maxSessions: number
55
+ }
56
+
57
+ interface DocRow {
58
+ key: string
59
+ body: string
60
+ kind: 'file' | 'session'
61
+ path: string
62
+ title: string
63
+ sessionId: string | null
64
+ messageId: string | null
65
+ mtime: number
66
+ }
67
+
48
68
  export class ContentIndex {
49
- private db: import('node:sqlite').DatabaseSync | null = null
50
- private readonly state: ContentState = { building: false, files: 0, sessions: 0, messages: 0, error: '', updatedAt: 0 }
69
+ private db: DatabaseSync | null = null
70
+ private readonly state: ContentState = { building: false, files: 0, sessions: 0, messages: 0, truncated: false, error: '', updatedAt: 0 }
51
71
  private generation = 0
72
+ private totalBytes = 0
73
+ private timer: NodeJS.Timeout | undefined
52
74
 
53
- constructor(private readonly rt: LocatorRuntime, private readonly options: {
54
- stateDir: string
55
- sessionDirs: string[]
56
- maxFileBytes: number
57
- maxFiles: number
58
- maxSessions: number
59
- }) {}
75
+ constructor(private readonly rt: LocatorRuntime, private readonly options: ContentOptions) {}
60
76
 
61
77
  status(): ContentState { return { ...this.state } }
62
78
 
63
- /** Close the database when the plugin unmounts. */
79
+ /** Start periodic incremental refreshes; 0 disables. */
80
+ startAutoRefresh(ms: number): void {
81
+ if (ms <= 0 || this.timer !== undefined) return
82
+ this.timer = setInterval(() => this.refresh(), ms)
83
+ }
84
+
85
+ /** Queue a full rebuild; false when one is already running. */
86
+ rebuild(): boolean {
87
+ if (this.state.building) return false
88
+ this.start()
89
+ return true
90
+ }
91
+
92
+ /** Queue an incremental refresh; false when one is already running. */
93
+ refreshNow(): boolean {
94
+ if (this.state.building) return false
95
+ this.refresh()
96
+ return true
97
+ }
98
+
64
99
  close(): void {
100
+ if (this.timer !== undefined) { clearInterval(this.timer); this.timer = undefined }
65
101
  this.generation++
66
102
  this.db?.close()
67
103
  this.db = null
68
104
  this.state.building = false
69
105
  }
70
106
 
71
- /** Build in the background; stale generations stop at their next checkpoint. */
72
- start(): void {
107
+ /** Build everything from scratch. */
108
+ start(): void { this.begin('full') }
109
+
110
+ /** Re-read changed files/sessions; open the DB when this is the first run. */
111
+ refresh(): void { this.begin(this.db === null ? 'full' : 'incremental') }
112
+
113
+ private begin(mode: 'full' | 'incremental'): void {
114
+ if (this.state.building) return
73
115
  const generation = ++this.generation
74
116
  this.state.building = true
75
117
  this.state.error = ''
76
- void this.build(generation).catch(error => {
118
+ this.state.truncated = false
119
+ const run = mode === 'full' ? this.full(generation) : this.incremental(generation)
120
+ void run.catch(error => {
77
121
  if (generation !== this.generation) return
78
122
  this.state.error = messageOf(error)
79
123
  this.state.building = false
80
124
  })
81
125
  }
82
126
 
83
- private async build(generation: number): Promise<void> {
84
- const { DatabaseSync } = await import('node:sqlite')
85
- const path = join(this.options.stateDir, 'content.db')
127
+ private async full(generation: number): Promise<void> {
128
+ this.open(true)
129
+ this.totalBytes = 0
130
+ await this.indexFiles(generation)
131
+ if (generation !== this.generation) return
132
+ await this.indexSessions(generation)
133
+ if (generation !== this.generation) return
134
+ this.finish()
135
+ }
136
+
137
+ private async incremental(generation: number): Promise<void> {
138
+ this.open(false)
139
+ this.totalBytes = Number((this.db!.prepare('SELECT COALESCE(SUM(length(body)), 0) AS n FROM docs').get() as { n: number }).n)
140
+ await this.refreshFiles(generation)
141
+ if (generation !== this.generation) return
142
+ await this.refreshSessions(generation)
143
+ if (generation !== this.generation) return
144
+ this.finish()
145
+ }
146
+
147
+ private open(fresh: boolean): void {
86
148
  mkdirSync(this.options.stateDir, { recursive: true })
87
149
  this.db?.close()
88
- this.db = new DatabaseSync(path)
150
+ this.db = new DatabaseSync(join(this.options.stateDir, 'content.db'))
89
151
  this.db.exec('PRAGMA journal_mode = WAL')
90
152
  this.db.exec('PRAGMA synchronous = OFF')
91
- this.db.exec('DROP TABLE IF EXISTS docs')
92
- this.db.exec(`CREATE VIRTUAL TABLE docs USING fts5(
93
- body,
94
- kind UNINDEXED,
95
- path UNINDEXED,
96
- title UNINDEXED,
97
- session_id UNINDEXED,
98
- message_id UNINDEXED,
99
- tokenize = 'trigram'
100
- )`)
101
- const insert = this.db.prepare('INSERT INTO docs (body, kind, path, title, session_id, message_id) VALUES (?, ?, ?, ?, ?, ?)')
102
- const flush = (rows: Array<[string, string, string, string, string | null, string | null]>) => {
103
- if (rows.length === 0) return
104
- this.db!.exec('BEGIN')
105
- try {
106
- for (const row of rows) insert.run(...row)
107
- this.db!.exec('COMMIT')
108
- } catch (error) {
109
- this.db!.exec('ROLLBACK')
110
- throw error
111
- }
153
+ if (fresh) {
154
+ this.db.exec('DROP TABLE IF EXISTS docs')
155
+ this.db.exec('DROP TABLE IF EXISTS docs_meta')
156
+ this.db.exec('DROP TABLE IF EXISTS sources')
112
157
  }
158
+ this.db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS docs USING fts5(
159
+ body, kind UNINDEXED, path UNINDEXED, title UNINDEXED,
160
+ session_id UNINDEXED, message_id UNINDEXED, tokenize = 'trigram'
161
+ )`)
162
+ this.db.exec('CREATE TABLE IF NOT EXISTS docs_meta (rowid INTEGER PRIMARY KEY, doc_key TEXT UNIQUE, path TEXT, mtime INTEGER, kind TEXT)')
163
+ this.db.exec('CREATE TABLE IF NOT EXISTS sources (path TEXT PRIMARY KEY, mtime INTEGER)')
164
+ }
113
165
 
114
- await this.indexSessions(flush, generation)
115
- if (generation !== this.generation) return
116
- await this.indexFiles(flush, generation)
117
- if (generation !== this.generation) return
166
+ private finish(): void {
167
+ const counts = this.db!.prepare("SELECT kind, COUNT(*) AS n FROM docs_meta GROUP BY kind").all() as Array<{ kind: string; n: number }>
168
+ this.state.files = Number(counts.find(row => row.kind === 'file')?.n ?? 0)
169
+ this.state.messages = Number(counts.find(row => row.kind === 'session')?.n ?? 0)
170
+ this.state.sessions = Number((this.db!.prepare('SELECT COUNT(*) AS n FROM sources').get() as { n: number }).n)
118
171
  this.state.building = false
119
172
  this.state.updatedAt = Date.now()
120
173
  }
121
174
 
122
- private async indexSessions(flush: (rows: Array<[string, string, string, string, string | null, string | null]>) => void, generation: number): Promise<void> {
123
- const files: Array<{ path: string; mtime: number }> = []
124
- for (const root of this.sessionRoots()) {
125
- walkSessions(root, files, 0)
126
- }
127
- files.sort((a, b) => b.mtime - a.mtime)
128
- const limit = this.options.maxSessions > 0 ? this.options.maxSessions : files.length
129
- let batch: Array<[string, string, string, string, string | null, string | null]> = []
130
- for (const file of files.slice(0, limit)) {
131
- if (generation !== this.generation) return
132
- let text: string
133
- try { text = zstdDecompressSync(readFileSync(file.path)).toString('utf8') } catch { continue }
134
- const sessionId = basename(dirname(file.path))
135
- let title = ''
136
- let messages = 0
137
- for (const line of text.split('\n')) {
138
- if (!line) continue
139
- let event: { type?: string; data?: Record<string, unknown> }
140
- try { event = JSON.parse(line) } catch { continue }
141
- const extracted = messageText(event)
142
- if (extracted === undefined) continue
143
- if (title === '') title = extracted.text.replace(/\s+/g, ' ').slice(0, 90)
144
- batch.push([extracted.text.slice(0, 8_000), 'session', file.path, title || sessionId, sessionId, extracted.id])
145
- messages++
146
- if (batch.length >= 200) { flush(batch); batch = [] }
147
- }
148
- this.state.sessions++
149
- this.state.messages += messages
175
+ private addDoc(row: DocRow): boolean {
176
+ const cap = this.options.maxTotalBytes ?? 0
177
+ if (cap > 0 && this.totalBytes + row.body.length > cap) {
178
+ this.state.truncated = true
179
+ return false
150
180
  }
151
- flush(batch)
181
+ this.deleteDoc(row.key)
182
+ const meta = this.db!.prepare('INSERT INTO docs_meta (doc_key, path, mtime, kind) VALUES (?, ?, ?, ?)').run(row.key, row.path, row.mtime, row.kind)
183
+ const rowid = meta.lastInsertRowid
184
+ this.db!.prepare('INSERT INTO docs (rowid, body, kind, path, title, session_id, message_id) VALUES (?, ?, ?, ?, ?, ?, ?)').run(rowid, row.body, row.kind, row.path, row.title, row.sessionId, row.messageId)
185
+ this.totalBytes += row.body.length
186
+ return true
152
187
  }
153
188
 
154
- private async indexFiles(flush: (rows: Array<[string, string, string, string, string | null, string | null]>) => void, generation: number): Promise<void> {
189
+ private deleteDoc(key: string): void {
190
+ const row = this.db!.prepare('SELECT rowid FROM docs_meta WHERE doc_key = ?').get(key) as { rowid: number } | undefined
191
+ if (row === undefined) return
192
+ this.db!.prepare('DELETE FROM docs WHERE rowid = ?').run(row.rowid)
193
+ this.db!.prepare('DELETE FROM docs_meta WHERE rowid = ?').run(row.rowid)
194
+ }
195
+
196
+ private deletePath(path: string): void {
197
+ const rows = this.db!.prepare('SELECT rowid FROM docs_meta WHERE path = ?').all(path) as Array<{ rowid: number }>
198
+ const delDoc = this.db!.prepare('DELETE FROM docs WHERE rowid = ?')
199
+ const delMeta = this.db!.prepare('DELETE FROM docs_meta WHERE rowid = ?')
200
+ for (const row of rows) { delDoc.run(row.rowid); delMeta.run(row.rowid) }
201
+ }
202
+
203
+ private async indexFiles(generation: number): Promise<void> {
155
204
  let after = 0
156
- let batch: Array<[string, string, string, string, string | null, string | null]> = []
157
205
  let seen = 0
158
206
  for (;;) {
159
207
  if (generation !== this.generation) return
@@ -162,27 +210,104 @@ export class ContentIndex {
162
210
  if (entry.dir || entry.size <= 0 || entry.size > this.options.maxFileBytes) continue
163
211
  if (!TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue
164
212
  if (this.options.maxFiles > 0 && seen >= this.options.maxFiles) break
165
- let body: string
166
- try { body = readFileSync(entry.path, 'utf8') } catch { continue }
167
- if (body.includes('\u0000')) continue
213
+ const row = this.readFileDoc(entry.path, entry.name, entry.mtime)
214
+ if (row === null) continue
168
215
  seen++
169
- this.state.files++
170
- batch.push([body.slice(0, this.options.maxFileBytes), 'file', entry.path, entry.name, null, null])
171
- if (batch.length >= 200) { flush(batch); batch = [] }
216
+ if (!this.addDoc(row)) return
217
+ }
218
+ after = page.next
219
+ if (page.done || page.entries.length === 0) break
220
+ }
221
+ }
222
+
223
+ private async refreshFiles(generation: number): Promise<void> {
224
+ const meta = new Map<string, number>()
225
+ for (const row of this.db!.prepare("SELECT doc_key, mtime FROM docs_meta WHERE kind = 'file'").all() as Array<{ doc_key: string; mtime: number }>) meta.set(row.doc_key, row.mtime)
226
+ const seen = new Set<string>()
227
+ let after = 0
228
+ let indexed = 0
229
+ for (;;) {
230
+ if (generation !== this.generation) return
231
+ const page = await this.rt.dump(after, 2_000)
232
+ for (const entry of page.entries) {
233
+ if (entry.dir || entry.size <= 0 || entry.size > this.options.maxFileBytes) continue
234
+ if (!TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue
235
+ if (this.options.maxFiles > 0 && indexed >= this.options.maxFiles) break
236
+ const key = `file:${entry.path}`
237
+ seen.add(key)
238
+ indexed++
239
+ if (meta.get(key) === entry.mtime) continue
240
+ this.deleteDoc(key)
241
+ const row = this.readFileDoc(entry.path, entry.name, entry.mtime)
242
+ if (row !== null && !this.addDoc(row)) return
172
243
  }
173
244
  after = page.next
174
245
  if (page.done || page.entries.length === 0) break
175
246
  }
176
- flush(batch)
247
+ for (const key of meta.keys()) if (!seen.has(key)) this.deleteDoc(key)
248
+ }
249
+
250
+ private readFileDoc(path: string, name: string, mtime: number): DocRow | null {
251
+ let raw: string
252
+ try { raw = readFileSync(path, 'utf8') } catch { return null }
253
+ if (raw.includes('\u0000')) return null
254
+ return { key: `file:${path}`, body: raw.slice(0, this.options.maxFileBytes), kind: 'file', path, title: name, sessionId: null, messageId: null, mtime }
255
+ }
256
+
257
+ private async indexSessions(generation: number): Promise<void> {
258
+ const files = sessionFiles(this.sessionRoots()).slice(0, this.options.maxSessions > 0 ? this.options.maxSessions : undefined)
259
+ for (const file of files) {
260
+ if (generation !== this.generation) return
261
+ this.deletePath(file.path)
262
+ if (!this.indexSessionFile(file)) return
263
+ this.db!.prepare('INSERT OR REPLACE INTO sources (path, mtime) VALUES (?, ?)').run(file.path, file.mtime)
264
+ }
265
+ }
266
+
267
+ private async refreshSessions(generation: number): Promise<void> {
268
+ const known = new Map<string, number>()
269
+ for (const row of this.db!.prepare('SELECT path, mtime FROM sources').all() as Array<{ path: string; mtime: number }>) known.set(row.path, row.mtime)
270
+ const files = sessionFiles(this.sessionRoots()).slice(0, this.options.maxSessions > 0 ? this.options.maxSessions : undefined)
271
+ const present = new Set<string>()
272
+ for (const file of files) {
273
+ if (generation !== this.generation) return
274
+ present.add(file.path)
275
+ if (known.get(file.path) === file.mtime) continue
276
+ this.deletePath(file.path)
277
+ if (!this.indexSessionFile(file)) return
278
+ this.db!.prepare('INSERT OR REPLACE INTO sources (path, mtime) VALUES (?, ?)').run(file.path, file.mtime)
279
+ }
280
+ for (const path of known.keys()) {
281
+ if (present.has(path)) continue
282
+ this.deletePath(path)
283
+ this.db!.prepare('DELETE FROM sources WHERE path = ?').run(path)
284
+ }
177
285
  }
178
286
 
179
- private sessionRoots(): string[] {
180
- if (this.options.sessionDirs.length > 0) return this.options.sessionDirs
181
- const home = process.env.DSH_HOME || join(homedir(), '.dsh')
182
- return [join(home, 'sessions')]
287
+ private indexSessionFile(file: { path: string; mtime: number }): boolean {
288
+ let text: string
289
+ try { text = zstdDecompressSync(readFileSync(file.path)).toString('utf8') } catch { return true }
290
+ const sessionId = basename(dirname(file.path))
291
+ let title = ''
292
+ for (const line of text.split('\n')) {
293
+ if (!line) continue
294
+ let event: { type?: string; data?: Record<string, unknown> }
295
+ try { event = JSON.parse(line) } catch { continue }
296
+ const extracted = messageText(event)
297
+ if (extracted === undefined) continue
298
+ if (title === '') title = extracted.text.replace(/\s+/g, ' ').slice(0, 90)
299
+ const body = extracted.text.slice(0, 8_000)
300
+ const row: DocRow = { key: `session:${sessionId}:${extracted.id}`, body, kind: 'session', path: file.path, title: title || sessionId, sessionId, messageId: extracted.id, mtime: file.mtime }
301
+ if (!this.addDoc(row)) return false
302
+ }
303
+ return true
304
+ }
305
+
306
+ sessionRoots(): string[] {
307
+ if (this.options.sessionDirs !== undefined) return this.options.sessionDirs
308
+ return [join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'sessions')]
183
309
  }
184
310
 
185
- /** Query the FTS table; short queries fall back to LIKE because trigram needs 3 chars. */
186
311
  search(query: string, limit: number): ContentHit[] {
187
312
  if (this.db === null) return []
188
313
  const text = query.trim()
@@ -231,22 +356,26 @@ function messageText(event: { type?: string; data?: Record<string, unknown> }):
231
356
  .join('\n')
232
357
  .trim()
233
358
  if (text === '') return undefined
234
- const id = event.type === 'user/message'
235
- ? String(data.id ?? '')
236
- : String(message.id ?? '')
359
+ const id = event.type === 'user/message' ? String(data.id ?? '') : String(message.id ?? '')
237
360
  return { text, id: id || `seq-${String(data.seq ?? '')}` }
238
361
  }
239
362
 
240
- function walkSessions(root: string, out: Array<{ path: string; mtime: number }>, depth: number): void {
241
- if (depth > 4 || !existsSync(root)) return
242
- let entries
243
- try { entries = readdirSync(root, { withFileTypes: true }) } catch { return }
244
- for (const entry of entries) {
245
- const path = join(root, entry.name)
246
- if (entry.isDirectory()) { walkSessions(path, out, depth + 1); continue }
247
- if (!entry.isFile() || !entry.name.endsWith('.v4.jsonl.zstd')) continue
248
- try { out.push({ path, mtime: statSync(path).mtimeMs }) } catch { /* ignore */ }
363
+ function sessionFiles(roots: string[]): Array<{ path: string; mtime: number }> {
364
+ const out: Array<{ path: string; mtime: number }> = []
365
+ const walk = (root: string, depth: number): void => {
366
+ if (depth > 4 || !existsSync(root)) return
367
+ let entries
368
+ try { entries = readdirSync(root, { withFileTypes: true }) } catch { return }
369
+ for (const entry of entries) {
370
+ const path = join(root, entry.name)
371
+ if (entry.isDirectory()) { walk(path, depth + 1); continue }
372
+ if (!entry.isFile() || !entry.name.endsWith('.v4.jsonl.zstd')) continue
373
+ try { out.push({ path, mtime: statSync(path).mtimeMs }) } catch { /* ignore */ }
374
+ }
249
375
  }
376
+ for (const root of roots) walk(root, 0)
377
+ out.sort((a, b) => b.mtime - a.mtime)
378
+ return out
250
379
  }
251
380
 
252
381
  function messageOf(error: unknown): string {
package/src/find.ts CHANGED
@@ -51,6 +51,7 @@ export function makeFindTool(content: ContentIndex) {
51
51
  files: { type: 'integer' },
52
52
  sessions: { type: 'integer' },
53
53
  messages: { type: 'integer' },
54
+ truncated: { type: 'boolean' },
54
55
  updatedAt: { type: 'integer' },
55
56
  error: { type: 'string' },
56
57
  },
package/src/http.ts CHANGED
@@ -71,6 +71,15 @@ export function registerHttp(ctx: Context, rt: LocatorRuntime, content?: Content
71
71
  handler: handler(scoped, async body => await reveal(rt, body)),
72
72
  }))
73
73
  if (content !== undefined) {
74
+ scoped.effect(() => server.register({
75
+ kind: 'exact',
76
+ path: '/dsh-locator/content/rebuild',
77
+ handler: handler(scoped, async body => {
78
+ const full = body.full === true
79
+ const started = full ? content.rebuild() : content.refreshNow()
80
+ return started ? { ok: true, started: true, full } : { ok: false, error: 'a content build is already running' }
81
+ }),
82
+ }))
74
83
  scoped.effect(() => server.register({
75
84
  kind: 'exact',
76
85
  path: '/dsh-locator/content',