dsh-locator 0.7.5 → 0.7.7

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/README.en.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  # 🔎 dsh-locator
4
4
 
5
- **Whole-disk, millisecond file search for DeepSeek Harness — without Everything.**
5
+ **Search names when you remember the name, contents when you remember the phrase — then open the file or jump back to the conversation.**
6
6
 
7
- A self-built cross-platform index (Windows USN/MFT · macOS FSEvents · Linux inotify), six agent tools — names, contents and sessions — and a sortable Web panel.
7
+ Whole-disk file search for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) with its **own** index: no Everything install, no `es.exe`, one set of tools and one panel across Windows, macOS and Linux.
8
8
 
9
9
  [![npm version](https://img.shields.io/npm/v/dsh-locator)](https://www.npmjs.com/package/dsh-locator)
10
10
  [![npm downloads](https://img.shields.io/npm/dm/dsh-locator)](https://www.npmjs.com/package/dsh-locator)
@@ -12,7 +12,8 @@ A self-built cross-platform index (Windows USN/MFT · macOS FSEvents · Linux in
12
12
  [![version](https://img.shields.io/github/v/tag/PerryLink/dsh-locator?label=version)](https://github.com/PerryLink/dsh-locator/releases)
13
13
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
14
14
  [![DSH plugin](https://img.shields.io/badge/dsh--plugin-✅-green)](https://github.com/topics/dsh-plugin)
15
- [![Node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-brightgreen.svg)](#)
15
+ [![Awesome DSH](https://img.shields.io/badge/awesome--dsh--plugin-listed-2ea44f)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
16
+ [![Node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-brightgreen.svg)](#version-and-compatibility)
16
17
  [![Gitee](https://img.shields.io/badge/Gitee-mirror-c71d23?logo=gitee)](https://gitee.com/perrylink/dsh-locator)
17
18
  [![GitCode](https://img.shields.io/badge/GitCode-mirror-1f6feb)](https://gitcode.com/PerryLink/dsh-locator)
18
19
 
@@ -24,19 +25,62 @@ dsh plugin --profile web add dsh-locator
24
25
 
25
26
  </div>
26
27
 
27
- > **Official repository:** [GitHub `PerryLink/dsh-locator`](https://github.com/PerryLink/dsh-locator). Gitee and GitCode are automatically synced mirrors; GitHub is the source of truth.
28
- >
29
- > Part of the [DSH plugin family](https://github.com/PerryLink) (Apache-2.0). If it helps, **a ⭐ helps the next person find it** — nothing is gated behind it.
30
-
31
- A whole-disk, millisecond file search for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) that builds its **own** index. No Everything installation, no `es.exe`, no Everything service.
28
+ > **Official repository:** [GitHub `PerryLink/dsh-locator`](https://github.com/PerryLink/dsh-locator); Gitee and GitCode are automatically synced mirrors, GitHub is the source of truth. Part of the [DSH plugin family](https://github.com/PerryLink) (Apache-2.0) and listed on [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin). A ⭐ helps the next person find it — nothing is gated behind it.
29
+
30
+ ## Contents
31
+
32
+ - [At a glance](#at-a-glance)
33
+ - [How it differs from Everything / Spotlight / grep](#how-it-differs-from-everything--spotlight--grep)
34
+ - [Install](#install)
35
+ - [First run](#first-run)
36
+ - [Two searches](#two-searches)
37
+ - [Web panel](#web-panel)
38
+ - [Tools](#tools)
39
+ - [Query language](#query-language)
40
+ - [Configuration](#configuration)
41
+ - [Performance and resources](#performance-and-resources)
42
+ - [Platform support](#platform-support)
43
+ - [Troubleshooting](#troubleshooting)
44
+ - [Build from source](#build-from-source)
45
+ - [Design](#design)
46
+ - [Limitations](#limitations)
47
+ - [Memory](#memory)
48
+ - [Security and privacy](#security-and-privacy)
49
+ - [Version and compatibility](#version-and-compatibility)
50
+ - [Roadmap](#roadmap)
51
+ - [License](#license)
52
+
53
+ ## At a glance
54
+
55
+ File lookup has two shapes:
56
+
57
+ - **You remember the name but not the location.** `glob`/`grep` walk the workspace on every call — fine when you know the directory, painful when you remember half a filename.
58
+ - **You remember a sentence, a decision or a snippet, not the file or conversation.** A name index cannot help, and DSH session logs are first-class data too.
59
+
60
+ `dsh-locator` puts both in one plugin:
61
+
62
+ | Capability | How you use it |
63
+ |---|---|
64
+ | File name / path search | `locator_search`, or the panel's default mode |
65
+ | File body search | `locator_find`, or the panel's **Search content** mode |
66
+ | DSH session search | Same tools; hits carry `sessionId` / `messageId`, so the panel can jump back to the conversation |
67
+ | Open / reveal | Tools return paths; the panel double-click reveals, `Ctrl+O` opens, `Ctrl+Enter` inserts a quoted `@"path"` into the composer |
68
+ | External consumption | `locator_export` writes a read-only SQLite mirror |
32
69
 
33
- `dsh-locator` scans the directories you choose once, keeps the result in a compact snapshot, applies filesystem changes as they happen, and answers name/path queries in tens of milliseconds across a million entries. The agent gets `locator_search`, `locator_status`, `locator_rebuild` and `locator_export`; people get a global **search panel** in the Web GUI (`Ctrl+Shift+F` or the magnifier at the sidebar foot) with sortable columns, filters, keyboard navigation and one-key path copy.
70
+ The index is local: no network calls, no telemetry, no uploads, and the plugin never modifies, moves or deletes indexed files.
34
71
 
35
- Everything inspired the *ideas*: a persistent index of file names, a query language with `ext:` / `size:` / `dm:` filters, and an updater that consumes the filesystem's own change stream. The implementation is independent and cross-platform.
72
+ ## How it differs from Everything / Spotlight / grep
36
73
 
37
- ## Why another file search
74
+ | | dsh-locator | Everything | Spotlight | `glob` / `grep` |
75
+ |---|---|---|---|---|
76
+ | Platforms | Windows / macOS / Linux / WSL | Windows | macOS | Cross-platform |
77
+ | Index | Persistent, self-built | Persistent (NTFS USN) | System metadata/content index | Re-walk per call |
78
+ | Body search | Text files + DSH session messages | Names only by default | Yes, varies by system | Yes, unindexed |
79
+ | Session jump | Hits carry `sessionId` / `messageId` | No | No | No |
80
+ | Agent tools | Six tools, one query language | Needs a CLI/service | Needs glue scripts | Native, no persistent index |
81
+ | Windows speed | Close to Everything (USN optional) | Fastest and lightest | N/A | Slower than an index |
38
82
 
39
- `glob` and `grep` walk the workspace on every call — fine when you know the directory, slow when you remember only part of a name. Everything is excellent on Windows but is Windows-only, and wrapping it leaves macOS, Linux, WSL and remote hosts uncovered. `dsh-locator` owns the index, so the same tools work everywhere Node and Go run.
83
+ In short: the **idea** behind Everything (persistent index + structured query) is implemented independently and cross-platform, with DSH sessions added on top. It is not a wrapper around Everything.
40
84
 
41
85
  ## Install
42
86
 
@@ -44,27 +88,32 @@ Everything inspired the *ideas*: a persistent index of file names, a query langu
44
88
  # npm (recommended)
45
89
  dsh plugin --profile web add dsh-locator
46
90
 
91
+ # pin a version
92
+ dsh plugin --profile web add dsh-locator@0.7.6
93
+
47
94
  # GitHub main
48
95
  dsh plugin --profile web add github:PerryLink/dsh-locator
49
96
 
50
97
  # from a checkout
51
98
  pnpm install
52
- pnpm build # TypeScript plugin
53
- pnpm build:engine # Go engine for the host platform
99
+ pnpm build
100
+ pnpm build:engine # Go engine for the host platform
54
101
  dsh plugin --profile web add /absolute/path/to/dsh-locator
55
102
 
56
- # from a packed tarball (verified install path)
103
+ # from a packed tarball (npm pack output; substitute the real version)
57
104
  npm pack
58
- dsh plugin --profile web add /absolute/path/to/dsh-locator-0.5.0.tgz
105
+ dsh plugin --profile web add /absolute/path/to/dsh-locator-0.7.6.tgz
59
106
  ```
60
107
 
61
- The npm package ships prebuilt engines for `linux-x64`, `linux-arm64`, `win32-x64`, `win32-arm64`, `darwin-x64` and `darwin-arm64`.
62
-
63
- After installing into a Web profile that is already running, **reload the page** (`F5`): the host tools may mount immediately, but the browser panel (`client.js`) is loaded from the boot graph at page load. Headless/TUI profiles never mount the panel.
108
+ - The package ships prebuilt engines for `linux-x64`, `linux-arm64`, `win32-x64`, `win32-arm64`, `darwin-x64` and `darwin-arm64`; no local Go is needed.
109
+ - If your npm mirror lags behind, add `--registry=https://registry.npmjs.org` to the install command.
110
+ - After installing into a **running** Web profile, **reload the page** (`F5`): host tools may mount immediately, but the browser panel is loaded from the boot graph at page load. Headless/TUI profiles get the tools only.
111
+ - Upgrade: `dsh plugin --profile web add dsh-locator@latest`, then `F5`. A content-schema change rebuilds the database once.
112
+ - Uninstall: `dsh plugin --profile web remove dsh-locator`; to keep the config but disable it, set `enabled: false`.
64
113
 
65
114
  ## First run
66
115
 
67
- On first mount the engine indexes the configured roots (default: your home directory) and writes a snapshot to the platform cache directory:
116
+ On first mount the engine indexes the configured roots (default: your home directory) and writes the snapshot and content database to the platform cache directory:
68
117
 
69
118
  | Platform | Default state directory |
70
119
  |---|---|
@@ -72,38 +121,73 @@ On first mount the engine indexes the configured roots (default: your home direc
72
121
  | macOS | `~/Library/Application Support/dsh-locator` |
73
122
  | Linux | `$XDG_CACHE_HOME/dsh-locator` or `~/.cache/dsh-locator` |
74
123
 
75
- The first scan of a large home directory reads the disk; later starts load the snapshot instead. Requests are served **during** that first scan: search returns the entries indexed so far with `indexing: true`, and the panel footer says the index is still building. Nothing blocks on the cold scan. A default exclude list skips `node_modules`, `.git`, `.svn`, `.hg`, `.cache`, `__pycache__`, `.venv`, `venv` and `.next`.
124
+ The first scan of a large home directory reads the disk; later starts load the snapshot. Requests are served **during** the first scan: search returns the entries indexed so far with `indexing: true`, and the panel footer says the index is still building.
125
+
126
+ Defaults skip `node_modules`, `.git`, `.hg`, `.svn`, `.cache`, `__pycache__`, `.venv`, `venv` and `.next` by name; `excludes` changes the list.
127
+
128
+ Things to try:
129
+
130
+ ```text
131
+ Find the PDFs I changed last week
132
+ Where was "contentRefreshMs" discussed?
133
+ Which conversation settled this design?
134
+ ```
135
+
136
+ ## Two searches
137
+
138
+ ### By name: `locator_search`
139
+
140
+ Everything-style syntax returning path, size, modification time, extension and kind. Suited to “the file name contains report”, “PDFs changed this week”, “videos over 50 MB”.
141
+
142
+ Sorting, pagination and `limit`/`offset` happen in the host engine; common queries over 1.51 M entries return in tens of milliseconds.
143
+
144
+ ### By content: `locator_find`
145
+
146
+ Searches the **bodies** of text files and DSH session messages and returns snippets plus their source:
147
+
148
+ - file hits carry a path and can be opened or revealed;
149
+ - session hits carry `sessionId` and `messageId`, so the panel can open the conversation and outline the matched message;
150
+ - Chinese and English substrings both work (trigram tokenizer, no word segmentation needed).
151
+
152
+ How the content index behaves:
153
+
154
+ - **Lazy by default.** Nothing is built at plugin start; the first content search (`locator_find` or the panel's content mode) starts the build in the background, and `contentLazy: false` restores the eager build. The first query reports `building: true` instead of an empty result with no hint.
155
+ - **Restart reuses the database.** An existing database is refreshed incrementally; a full build (session logs first, then file bodies) runs only when the database is missing, has a stale schema, was left unfinished by an interrupted build, or cannot be opened.
156
+ - **Incremental refresh** every `contentRefreshMs` (5 minutes by default) re-reads only files and sessions whose mtime changed; `0` disables it.
157
+ - **Archived and active sessions.** Archived sessions are skipped unless `contentIncludeArchived: true`; a session log that is still being written is indexed after it has been quiet for `contentActiveSessionDelayMs` (2 minutes), so the current log is not decompressed on every refresh. Deleted sessions leave the index when their files disappear.
158
+ - **Budget and truncation.** Defaults: 1 MB per text file, 200,000 files, 1 GB of body bytes, the 100 most recent sessions. Once the byte budget is reached, files later in scan order are skipped; `locator_find` reports `content budget reached` and the HTTP state carries `truncated: true`. Raise `contentMaxTotalBytes` or narrow `contentExcludes`.
159
+ - **Dependency trees are not read.** `node_modules`, `.cargo`, `.bun`, `.pnpm`, `site-packages`, `toolchain`, … are skipped by `contentExcludes` by default — otherwise library code would exhaust the budget before your own documents; the name index still covers them.
76
160
 
77
- ## Web search panel
161
+ The build runs inside the DSH host process, but file reads and zstd decoding are asynchronous and both build loops yield about every 8 ms: on a 28.6 MB corpus the longest event-loop stall dropped from 968 ms to about 20 ms, so session loading and switching stay responsive. Content **queries** run in a worker thread with a read-only connection — common words take roughly 100 ms, rare terms a few milliseconds, and the host is never blocked.
162
+
163
+ ## Web panel
78
164
 
79
165
  ![dsh-locator search panel: content mode checked, showing body matches and snippets](docs/assets/panel-en.png)
80
166
 
81
- Press `Ctrl+Shift+F` anywhere in the Web GUI, or click the magnifier at the sidebar foot. The overlay queries the same index through two same-origin host routes (`POST /dsh-locator/search`, `POST /dsh-locator/status`), so it needs no second server and no extra port.
167
+ Press `Ctrl+Shift+F` anywhere in the Web GUI, or click the magnifier at the sidebar foot.
168
+
169
+ - Empty query lists the most recently modified entries; typing runs the same Everything-style syntax after a 110 ms debounce.
170
+ - Click a column header to sort by name, size, modification time or path.
171
+ - Toggle **files only** / **directories only** to filter the kind.
172
+ - Tick **Search content** to switch from names to file bodies and session messages; while the index builds the footer shows the state and the panel re-polls.
173
+ - `↑`/`↓` select, `Enter` copies the path, `Ctrl+O` opens, `Ctrl+Enter` inserts a quoted `@"path"` into the composer, `Esc` closes.
174
+ - Double-click a file hit to reveal it in Explorer/Finder/`xdg-open`; double-click a session hit to open the conversation at the matched message.
175
+ - Panel text follows `navigator.language` (zh/en/es/pt/hi).
82
176
 
83
- * Empty query lists the most recently modified entries; typing runs the same Everything-style syntax as the tool after a 110 ms debounce.
84
- * Click a column header to sort by name, size, modification time or path; toggle ascending/descending.
85
- * `只看文件` / `只看目录` filter the kind; the footer reports matches, the page cap and query latency.
86
- * Tick **Search content** (`搜内容`) to switch from names to file bodies and session messages; hits show a one-line snippet, and a session hit carries its conversation.
87
- * `↑`/`↓` select, `Enter` copies the selected path, `Ctrl+O` opens it, `Ctrl+Enter` inserts a quoted `@"path"` into the composer, **double-click reveals a file** in Explorer/Finder/`xdg-open`, and **double-click opens a session hit** at the matched message, outlined for a moment, `Esc` closes.
88
- * The panel follows `navigator.language` (zh/en/es/pt/hi).
89
- * The panel is optional: profiles without a Web server never mount it, and the three tools keep working.
177
+ The panel talks to same-origin host routes — `/dsh-locator/search`, `/dsh-locator/content`, `/dsh-locator/content/rebuild`, `/dsh-locator/status`, `/dsh-locator/reveal`, `/dsh-locator/open` — so it needs no second server and no extra port. Profiles without a Web server never mount it.
90
178
 
91
179
  ## Tools
92
180
 
93
181
  | Tool | What it does |
94
182
  |---|---|
95
- | `locator_search` | Query the index. Returns path, size, modification time, extension and kind, plus total/truncated so a capped page is never presented as complete. |
96
- | `locator_find` | Search file bodies and DSH session messages. Hits return a snippet plus `sessionId`/`messageId` for conversations. |
97
- | `locator_status` | Roots, entry counts, watcher state, state directory and engine path. Use it to explain coverage or diagnose a stale result. |
98
- | `locator_rebuild` | Replace the index with a fresh background scan. Searches keep using the old index until the new one is ready. |
183
+ | `locator_search` | Query names and paths, with `ext:`, `size:`, `dm:`, `path:` filters plus sorting and pagination. |
184
+ | `locator_find` | Search file bodies and session messages; hits carry snippets and `sessionId`/`messageId`, plus the content-index state. |
185
+ | `locator_status` | Roots, name-index counts, watcher state, state directory and engine path. (Content progress is reported by `locator_find` and the panel, not here.) |
186
+ | `locator_rebuild` | Replace the name index with a fresh background scan; searches keep using the old index until the new one is ready. |
99
187
  | `locator_reindex` | Refresh or rebuild the content index: incremental by default, `full: true` drops and rebuilds it. |
100
- | `locator_export` | Write a read-only SQLite mirror of the index (`entries` table) for external SQL tools. Streams page by page; the binary snapshot stays primary. |
101
-
102
- ### Content search
188
+ | `locator_export` | Write a read-only SQLite mirror of the name index (`entries` table) for external SQL tools. |
103
189
 
104
- `locator_find` searches the **bodies** of text files and DSH session messages through a local SQLite FTS5 index. Hits return a snippet plus `sessionId`/`messageId` for conversations, so the panel can open the session and outline the matched message. The content index is **lazy by default**: nothing is built at plugin start, and the first body/session search (`locator_find` or the panel's content mode) starts the build in the background — `contentLazy: false` restores the eager build. A restart with an existing database refreshes it incrementally on first use; a full build (session logs first, then file bodies) only runs when the database is missing, carries a stale schema version or cannot be opened. An incremental refresh then follows every `contentRefreshMs` (5 minutes), re-reading only files and sessions whose mtime changed; `locator_reindex` triggers either mode manually (`full: true` drops and rebuilds the database). While a build runs, searches keep answering from what is already indexed and `state.building` reports progress. Text files are capped by `contentMaxFileBytes`, the total body store by `contentMaxTotalBytes`, and only the most recent `contentMaxSessions` session logs are indexed. Dependency and toolchain trees (`node_modules`, `.cargo`, `.bun`, `.pnpm`, `site-packages`, `toolchain`, …) are skipped by default — otherwise they exhaust the byte budget before your own documents — and `contentExcludes` changes that list. The index runs inside the DSH host process, but file reads and zstd decoding are asynchronous and both build loops yield about every 8 ms: on a 28.6 MB corpus the longest event-loop stall dropped from 968 ms to 19 ms, so session loading and switching stay responsive. Content queries themselves run in a worker thread with a read-only connection, so a slow search cannot freeze the host while you type. Sessions deleted from disk leave the index on the next refresh; archived sessions are skipped unless `contentIncludeArchived: true`; a session log that is still being written is deferred until it has been quiet for `contentActiveSessionDelayMs`, instead of being fully decompressed on every refresh.
105
-
106
- ### Query language
190
+ ## Query language
107
191
 
108
192
  Bare words are ANDed substrings of the file name. Everything-style filters combine with them:
109
193
 
@@ -125,56 +209,92 @@ Bare words are ANDed substrings of the file name. Everything-style filters combi
125
209
 
126
210
  ## Configuration
127
211
 
128
- Every field is a Schemastery config field; edit the `dsh-locator` entry in the profile patch (or the bundled `cordis.patch.yml` defaults).
212
+ Every field is a Schemastery config field; override the `dsh-locator` entry in the profile patch, for example:
213
+
214
+ ```yaml
215
+ - id: dsh-locator
216
+ name: dsh-locator
217
+ config:
218
+ roots: ['/home/me/Documents', '/home/me/notes']
219
+ contentMaxTotalBytes: 2000000000
220
+ contentExcludes: ['node_modules', '.git', '.cargo', 'site-packages']
221
+ ```
129
222
 
130
223
  | Key | Default | Meaning |
131
224
  |---|---|---|
132
- | `enabled` | `true` | Mount no tools and start no engine when `false`. |
133
- | `roots` | `[]` | Directories to index; empty means the home directory. |
134
- | `excludes` | dev/cache directories | Base-name globs skipped by scanner and watcher. |
225
+ | `enabled` | `true` | Master switch; `false` mounts no tools and starts no engine. |
226
+ | `roots` | home directory | Directories to index; several may be given. |
227
+ | `excludes` | dev/cache names | Base-name globs skipped by the name index. |
135
228
  | `watch` | `true` | Apply native filesystem notifications. |
136
229
  | `refreshIntervalMs` | `1800000` | Rebuild cadence when no native watcher is active (kqueue fallback or a watcher failure); `0` disables. |
137
230
  | `scanWorkers` | `0` | Directory reader concurrency during scans; `0` uses the CPU count. |
231
+ | `windowsUsn` | `false` | Try the NTFS USN/MFT enumeration before the walk (Windows, administrator; falls back automatically). |
138
232
  | `memoryLimitMB` | `0` | Soft Go heap limit for the engine in MiB; `0` leaves it to `GOMEMLIMIT`/`GOGC`. |
139
- | `contentIndex` | `true` | Build the file-body and session-message index at all. |
140
- | `contentLazy` | `true` | Build the content index on the first content search instead of at plugin start; `false` builds it eagerly in the background. |
233
+ | `contentIndex` | `true` | Build the file-body and session-message index at all; `false` leaves name search with near-zero content overhead. |
234
+ | `contentLazy` | `true` | Build the content index on the first content search instead of at plugin start. |
141
235
  | `contentMaxFileBytes` | `1000000` | Bytes read from one text file. |
142
236
  | `contentMaxTotalBytes` | `1000000000` | Total body bytes kept in the content database; `0` means unlimited. |
143
237
  | `contentMaxFiles` | `200000` | Text files indexed per build; `0` means unlimited. |
144
238
  | `contentMaxSessions` | `100` | Most recent session logs indexed; `0` means all. |
145
239
  | `contentRefreshMs` | `300000` | Incremental content refresh cadence; `0` disables it. |
146
- | `contentIncludeArchived` | `false` | Index archived sessions as well; off by default. |
240
+ | `contentIncludeArchived` | `false` | Index archived sessions as well. |
147
241
  | `contentActiveSessionDelayMs` | `120000` | Only index a session log after it has been quiet this long. |
148
242
  | `contentSessionDirs` | `[]` | Extra session directories; empty means `$DSH_HOME/sessions`. |
149
243
  | `contentExcludes` | dependency/toolchain names | Path segments or substrings the content index skips; the name index still covers them. |
150
- | `windowsUsn` | `false` | Try the NTFS USN/MFT enumeration before the walk (Windows, administrator; falls back automatically). |
151
- | `stateDir` | `''` | Snapshot directory; empty means the platform cache directory. |
152
- | `binaryPath` | `''` | Override the bundled engine (development). |
153
- | `maxResults` | `100` | Hard cap for one search page. |
154
- | `startupTimeoutMs` | `120000` | How long a cold build may take before the plugin reports failure. |
155
- | `requestTimeoutMs` | `15000` | Per-request deadline. |
244
+ | `stateDir` | `''` | Snapshot and content database directory; empty means the platform cache directory. Use a separate value per DSH process. |
245
+
246
+ `binaryPath`, `maxResults`, `startupTimeoutMs` and `requestTimeoutMs` are also available for custom engines, page caps and deadlines; the defaults are fine for most setups.
247
+
248
+ ## Performance and resources
249
+
250
+ Measured on a WSL2 development machine (Linux, warm page cache) and a real home directory; your machine will differ with disk, file count, cache state and GC.
251
+
252
+ **Name index**
253
+
254
+ | Metric | Result |
255
+ |---|---|
256
+ | Cold scan | ~1.51 M entries in 11–12 s |
257
+ | Snapshot size | ~171 MB for 1.51 M entries |
258
+ | Hot query (`locator_search`) | 6–25 ms for common queries; `path:` prefix scans ~70 ms |
259
+ | Search while a rebuild runs | The old index keeps serving until the new one is ready |
260
+ | Peak scan memory | Hundreds of MB; the engine is a child process and honours `memoryLimitMB` / `GOMEMLIMIT` |
261
+
262
+ **Content index** (real home, 1 GB budget)
263
+
264
+ | Metric | Result (0.7.x, real home) |
265
+ |---|---|
266
+ | Coverage | ~95.8 k text files / 95 sessions / 1,050 messages |
267
+ | Full rebuild | ~3.5 min in the background; host p95 14 ms / max 115 ms during it |
268
+ | Incremental refresh (no change) | ~12 s (dominated by the engine dump of 1.51 M entries) |
269
+ | Idle host latency | p50 2 ms / p95 3 ms / max 6 ms |
270
+ | Database size | ~3.1 GB (about 3× the body budget) |
271
+ | Content query | ~100 ms for common words, a few ms for rare terms (worker thread, host unaffected) |
156
272
 
157
- Narrow `roots` if indexing the whole home directory is too much I/O. A small, focused set (`~/Documents`, `~/Downloads`, one code tree) is usually the best trade-off.
273
+ **Cheaper configurations**
274
+
275
+ - Name search only: `contentIndex: false` drops content work to zero.
276
+ - Keep content but use less disk: lower `contentMaxTotalBytes` (e.g. 256000000) or `contentMaxFileBytes`, or raise `contentRefreshMs`.
277
+ - Cover more files: raise `contentMaxTotalBytes` / `contentMaxFiles` and check that `contentExcludes` does not exclude real document trees.
278
+ - The content database is a cache: deleting `stateDir` rebuilds it and never touches the source files.
158
279
 
159
280
  ## Platform support
160
281
 
161
282
  | Platform | Scan + snapshot | Live updates | Notes |
162
283
  |---|---|---|---|
163
284
  | Linux | yes | inotify, recursive | Watch descriptors are per directory; very large trees can hit `fs.inotify.max_user_watches`. |
164
- | Windows | yes | `ReadDirectoryChangesW`, recursive | No admin rights and no Everything needed. Optional USN/MFT enumeration when running elevated. Changes made by WSL to `/mnt/<drive>` may not raise notifications; use `locator_rebuild` for those. |
165
- | macOS | yes | FSEvents in the shipped cgo binaries, recursive and uncapped | kqueue is the `CGO_ENABLED=0` fallback (up to 4,096 directories). File content changes do not raise vnode events in the fallback, so size/mtime refresh comes from `refreshIntervalMs` or `locator_rebuild`. |
166
-
167
- The engine is pure Go except the macOS FSEvents backend, which uses cgo and is built on a macOS runner. Linux, Windows and the kqueue fallback build with `CGO_ENABLED=0`; USN is optional and needs admin rights, everything else does not.
285
+ | Windows | yes | `ReadDirectoryChangesW`, recursive | No admin rights and no Everything needed. Optional USN/MFT enumeration when running elevated. Changes made by WSL to `/mnt/<drive>` may not raise notifications; rebuild those roots. |
286
+ | macOS | yes | FSEvents in the shipped cgo binaries, recursive and uncapped | kqueue is the `CGO_ENABLED=0` fallback (up to 4,096 directories); content changes rely on the periodic rebuild. |
168
287
 
169
288
  ### Windows and the USN journal
170
289
 
171
290
  The engine carries a USN decoder, an opt-in fast path and a probe. The path is **off by default** (`windowsUsn: false`) because the volume ioctl needs administrator rights; it has been validated end to end on a GitHub Actions Windows runner (administrator, real NTFS volume):
291
+
172
292
  - 1,345,546 MFT records read, 1,345,539 paths resolved
173
293
  - 164,209 entries under `C:\Windows` (122,915 files, 41,294 dirs)
174
294
  - `scan --usn --root C:\Windows` indexed 164,216 entries in 7.8 s; a query returned in 9.6 ms
175
295
  - the change journal was read (8,233 records on one run) and an inactive journal on `D:` fell back to the walk with a warning
176
296
 
177
- You can still run the same probe yourself:
297
+ Probe it yourself:
178
298
 
179
299
  ```powershell
180
300
  # Elevated PowerShell (or: Start-Process -Verb RunAs <path> ...)
@@ -183,41 +303,42 @@ You can still run the same probe yourself:
183
303
 
184
304
  It prints the journal id/USNs, enumerates the MFT (records per second, files/dirs below the root, sample paths) and decodes the last few change-journal records. A non-elevated run fails with `Access is denied` — the parser and path reconstruction are covered by unit tests on every platform.
185
305
 
186
- With `windowsUsn: true` (or `indexd scan --usn`) the engine enumerates the MFT instead of walking directories, then builds paths from the FRN parent map. A successful USN scan also writes `usn-state.json` (journal id, next USN, roots) and keeps the FRN in each record; on the next `serve --usn` the engine replays the journal from that cursor before answering, so files created while it was stopped appear without a full scan. A wrapped, deleted or mismatched journal falls back to the portable watcher. It keeps the volume handle and polls the change journal every two seconds, applying creates, deletes and renames through `usn.ApplyChanges` (the same FRN map, updated in place). USN records carry **no size or modification time**: results are correct for names and paths, `locator_status` marks such an index with `USN`, and the daemon stats the returned page so the rows the panel/model sees still show real sizes and dates. `size:`/`dm:` filters over the whole index stay approximate until a metadata pass runs. Any failure (no elevation, unsupported volume, too many records) falls back to the portable walk with a warning; it never disables search. If journal polling fails mid-session, the watcher reports it and stops; the portable `ReadDirectoryChangesW` watcher and `locator_rebuild` are still there. The journal applier is covered by synthetic-record tests and the ioctl path by the Windows CI job.
306
+ With `windowsUsn: true` (or `indexd scan --usn`) the engine enumerates the MFT instead of walking directories, then builds paths from the FRN parent map. A successful USN scan also writes `usn-state.json` (journal id, next USN, roots) and keeps the FRN in each record; on the next `serve --usn` the engine replays the journal from that cursor before answering, so files created while it was stopped appear without a full scan. A wrapped, deleted or mismatched journal falls back to the portable watcher. It keeps the volume handle and polls the change journal every two seconds, applying creates, deletes and renames through `usn.ApplyChanges` (the same FRN map, updated in place). USN records carry **no size or modification time**: results are correct for names and paths, `locator_status` marks such an index with `USN`, and the daemon stats the returned page so the rows still show real sizes and dates; `size:`/`dm:` filters over the whole index stay approximate until a metadata pass runs. Any failure (no elevation, unsupported volume, too many records) falls back to the portable walk with a warning; it never disables search.
187
307
 
188
308
  Everything reads the NTFS Master File Table through the USN journal, which is why its first index is nearly instant. `dsh-locator` keeps a portable directory walk as the default (no service, no UAC, any filesystem) and uses USN/MFT as an opt-in fast path where the process holds `GENERIC_READ` (admin), with journal replay across restarts. The portable path stays the fallback.
189
309
 
190
- ## Performance
310
+ ## Troubleshooting
191
311
 
192
- Measured on a developer WSL2 machine with a Linux home directory (1,234,757 entries: 1,128,288 files, 106,469 directories), excluding `node_modules`, `.git`, `.cache` and `.venv`:
312
+ **The panel does not appear after installing.**
313
+ Reload the page (`F5`); confirm you installed into the `web` profile and that the panel is loaded from the boot graph. Headless/TUI profiles never mount a panel.
193
314
 
194
- | Metric | Result |
195
- |---|---|
196
- | Cold scan (uncached I/O) | 50.3 s |
197
- | Warm scan (page cache, 8 readers) | 4.6–6.5 s (~190–265k entries/s) |
198
- | Snapshot size | 133 MB |
199
- | Peak scan RSS | ~490 MB |
200
- | First search during the initial scan | 2 ms (partial, `indexing: true`) |
201
- | `locator_search "report"` (981 matches) | 34 ms |
202
- | `locator_search "package.json"` (1,287 matches) | 25 ms |
203
- | Query process RSS (1.2 M entries) | ~197 MB |
204
- | Small tree (39k entries) scan | 0.47 s |
315
+ **A file created a moment ago is not found.**
316
+ Notifications may not have arrived yet (common for WSL writes into `/mnt/c`, network shares and some container overlays). Wait a moment or run `locator_rebuild` for that root. USN mode replays the journal on restart.
205
317
 
206
- Search is parallel over the record array; the slower cold scan is disk-bound. Numbers vary with the filesystem, cache state and CPU.
318
+ **Content search returns nothing.**
319
+ Check `building` in the `locator_find` result: the first search triggers the lazy build and results may be partial until it finishes (the panel re-polls automatically). Also confirm the file extension is a text type, the file is under `contentMaxFileBytes`, it is not excluded by `contentExcludes`, and the session is not archived (archived sessions are skipped by default).
207
320
 
208
- ### Content search
321
+ **The content database is huge / disk grows fast.**
322
+ See [Performance and resources](#performance-and-resources): lower the budget or set `contentIndex: false`. Deleting `stateDir` triggers a rebuild.
209
323
 
210
- Measured on the same machine, warm page cache:
324
+ **An `inotify` warning appears.**
325
+ Too many directories hit the kernel watch limit; the index stays usable and changes rely on periodic rebuilds. Raise `fs.inotify.max_user_watches` or reduce `roots`.
211
326
 
212
- | Corpus | Full build | `content.db` | Incremental (no change) | Query p50 / p95 |
213
- |---|---|---|---|---|
214
- | 20k files / 40 MB of bodies | 15.1 s | 71 MB | 0.17 s | 5 / 17 ms |
215
- | 200k files / 331 MB of bodies | 31.5 s | 711 MB | 1.3 s | 5 / 156 ms |
216
- | Real home, 177k documents at the 2 GB cap | 7.4 min | 5.99 GB | 2.9 s | — |
327
+ **Behaviour changed after an upgrade.**
328
+ Run `dsh plugin --profile web add dsh-locator@latest`, then `F5`. A content-schema change rebuilds the database once. See [CHANGELOG](CHANGELOG.md).
217
329
 
218
- * The database is roughly 2–3× the indexed body bytes; leave disk headroom. Dependency and toolchain trees (`contentExcludes`) are skipped by default — otherwise they eat the byte budget before your own documents.
219
- * A restart reuses `content.db` and refreshes incrementally (63.4 s → 4.6 s on the 200k corpus). A full rebuild only runs on the first start after an upgrade, a stale schema or an unreadable database.
220
- * When `contentMaxTotalBytes` is reached, `locator_find` says “content budget reached”; raise the cap or narrow `contentExcludes`.
330
+ ## Build from source
331
+
332
+ Requirements: Node `^22.19 || >=24.9`, pnpm, Go 1.22+ for the engine.
333
+
334
+ ```sh
335
+ pnpm install
336
+ pnpm build
337
+ pnpm build:engine:all # or build:engine for the host target
338
+ pnpm check # typecheck + tests + packaging/install verification
339
+ ```
340
+
341
+ The engine is a separate Go module under `engine/`; run its tests with `cd engine && go test ./...`. The macOS FSEvents backend needs cgo and is built on macOS CI. Node tests run on Linux, Windows and macOS in CI.
221
342
 
222
343
  ## Design
223
344
 
@@ -226,9 +347,12 @@ Measured on the same machine, warm page cache:
226
347
  ```
227
348
  DeepSeek Harness
228
349
  └─ dsh-locator (TypeScript plugin)
229
- ├─ locator_search / locator_status / locator_rebuild
350
+ ├─ locator_search / locator_find / locator_status / locator_rebuild /
351
+ │ locator_reindex / locator_export
230
352
  ├─ client.js: Ctrl+Shift+F overlay (shell.overlay slot)
231
- ├─ POST /dsh-locator/{search,status} (same-origin, loopback-hosted)
353
+ ├─ POST /dsh-locator/{search,content,status,reveal,open} (same-origin)
354
+ ├─ content store: SQLite FTS5 (trigram) in the host process,
355
+ │ queries in a worker thread
232
356
  └─ child process: indexd serve (Go, NDJSON over stdio)
233
357
  ├─ scanner: recursive walk, excludes, progress
234
358
  ├─ index: names arena + fixed records + open-addressing path table
@@ -237,56 +361,56 @@ DeepSeek Harness
237
361
  └─ snapshot: single file, mmap-friendly, atomic rename
238
362
  ```
239
363
 
240
- * **Parallel scanner.** Directory readers drain a bounded work queue and flush one batch per directory (`AddBatch`), so a parent record exists before its children are enqueued. The queue never blocks: a full queue falls back to scanning that subtree inline.
241
- * **Memory layout.** Records store a name offset/length into one byte arena plus a parent id; full paths are reconstructed only for the result page and for `path:` filters. That avoids one string header per file.
242
- * **Path table.** An open-addressing table of `id+1` slots keeps lookup/update near 12 bytes per entry instead of the ~50 bytes a `map[string]int32` costs.
243
- * **Query engine.** Bare terms and filters are compiled once, matched in parallel chunks, then reduced with a bounded max-heap: the top `offset+limit` rows are selected without sorting a million matches.
244
- * **Persistence.** The snapshot is written to a temporary file and renamed; a generation counter lets the daemon autosave only when the index changed. Tombstones are compacted when deletions exceed a threshold.
245
- * **Protocol.** One JSON request/response per line on stdio, with unsolicited `scan`, `stats`, `warning`, `error` and `rebuilt` events. No ports, no auth surface, no network listeners.
246
- * **Read-only.** The engine opens files for metadata only; it never writes to indexed paths.
364
+ - **Parallel scanner.** Directory readers drain a bounded work queue and flush one batch per directory (`AddBatch`), so a parent record exists before its children are enqueued. The queue never blocks: a full queue falls back to scanning that subtree inline.
365
+ - **Memory layout.** Records store a name offset/length into one byte arena plus a parent id; full paths are reconstructed only for the result page and for `path:` filters. That avoids one string header per file.
366
+ - **Path table.** An open-addressing table of `id+1` slots keeps lookup/update near 12 bytes per entry instead of the ~50 bytes a `map[string]int32` costs.
367
+ - **Query engine.** Bare terms and filters are compiled once, matched in parallel chunks, then reduced with a bounded max-heap: the top `offset+limit` rows are selected without sorting a million matches.
368
+ - **Content index.** Host-side SQLite FTS5 with the trigram tokenizer (UTF-8 substrings, CJK); the file list comes from the engine's own scan, session logs are decompressed with Node's zstd.
369
+ - **Content consistency.** A per-process build lock serializes writers sharing a `stateDir`, a completion flag forces a rebuild after an interrupted build, and a stale open handle is detected via device+inode so another process's rebuild cannot leave the host reading an unlinked database.
370
+ - **Persistence.** The snapshot is written to a temporary file and renamed; a generation counter lets the daemon autosave only when the index changed. Tombstones are compacted when deletions exceed a threshold.
371
+ - **Protocol.** One JSON request/response per line on stdio, with unsolicited `scan`, `stats`, `warning`, `error` and `rebuilt` events. No ports, no auth surface, no network listeners.
372
+ - **Read-only.** The engine opens files for metadata only; it never writes to indexed paths.
247
373
 
248
- ## Build from source
374
+ ## Limitations
249
375
 
250
- Requirements: Node `^22.19 || >=24.9`, pnpm, Go 1.22+ for the engine.
376
+ - Content search reads text files (by extension) and DSH session logs only; PDFs, Office documents and images are not text-extracted.
377
+ - The content budget is consumed in scan order, not by file importance; files later in that order are not searchable once the cap is reached (`truncated` is true).
378
+ - The index covers configured roots; it is not a filesystem driver.
379
+ - Linux inotify watches one descriptor per directory; a home directory with hundreds of thousands of directories can exceed the kernel limit. The plugin reports the warning and the snapshot stays usable; rebuilds catch up.
380
+ - WSL writes into `/mnt/c` may not produce Windows notifications (the Windows watcher cannot see the 9p writer's change stream). Rebuild those roots explicitly.
381
+ - Network shares are scanned only if they are reachable as ordinary directories; no SMB-specific change feed.
382
+ - Session content comes from local logs; messages not yet flushed to disk are not searchable.
383
+ - Two DSH processes sharing one `stateDir` should be avoided: the build lock serializes writers but cannot share one open database handle.
384
+ - macOS uses FSEvents (recursive) in the shipped binaries; a `CGO_ENABLED=0` build falls back to kqueue, capped at 4,096 directories, with content changes relying on the periodic rebuild.
385
+ - Windows USN/MFT is optional and only used when the process can read the volume and the journal is active; otherwise the portable walk is used.
251
386
 
252
- ```sh
253
- pnpm install
254
- pnpm typecheck
255
- pnpm test
256
- pnpm build
257
- pnpm build:engine:all # or build:engine for the host target
258
- ```
387
+ ## Memory
259
388
 
260
- The engine is a separate Go module under `engine/`; run its tests with `cd engine && go test ./...`.
389
+ The engine is a single Go process and honours the standard `GOMEMLIMIT` and `GOGC` environment variables (or `memoryLimitMB` in the config). A 1.2–1.5 M-entry index peaks around several hundred MB of RSS with the default GC; set `GOMEMLIMIT=600MiB` (soft limit, the runtime trades CPU for memory) or `GOGC=50` when the host is tight on RAM. Neither changes the snapshot format. In the live setup used for the numbers above, the DSH host process (including the content database page cache) sat around 700 MB RSS with the content index built, and idle host latency stayed at p50 2 ms.
261
390
 
262
- ## Limitations
391
+ ## Security and privacy
392
+
393
+ - The index is local: no network calls, no telemetry, no uploads. The plugin never modifies, moves or deletes indexed files.
394
+ - The content database and build lock are created `0600` and the state directory `0700`, so other local users cannot read indexed documents or session text.
395
+ - The snapshot and content database contain file names, paths and bodies; treat the state directory as private data. Deleting it clears the cache.
396
+ - Give concurrent DSH processes separate `stateDir` values.
263
397
 
264
- * Content search reads text files (by extension) and DSH session logs only; PDFs, Office documents and images are not text-extracted. The body store is capped at 2 GB / 200,000 files by default, so a huge home directory keeps only the first slice it reaches.
265
- * The index covers configured roots; it is not a filesystem driver.
266
- * Linux inotify watches one descriptor per directory; a home directory with hundreds of thousands of directories can exceed the kernel limit. The plugin reports the warning and the snapshot stays usable; rebuilds catch up.
267
- * WSL writes into `/mnt/c` may not produce Windows notifications (the Windows watcher cannot see the 9p writer's change stream). Rebuild those roots explicitly.
268
- * Network shares are scanned only if they are reachable as ordinary directories; no SMB-specific change feed.
269
- * macOS uses FSEvents (recursive) in the shipped binaries; a `CGO_ENABLED=0` build falls back to kqueue, capped at 4,096 directories. Content-only changes rely on the periodic rebuild either way.
270
- * Windows USN/MFT is optional and only used when the process can read the volume and the journal is active; otherwise the portable walk is used.
398
+ ## Version and compatibility
399
+
400
+ - Node `^22.19.0 || >=24.9.0`; DSH `>=0.2.1-rc.2 <0.3.0` (see `engines` in `package.json`).
401
+ - Releases are published with GitHub Actions provenance, so the npm package's origin can be verified; Gitee / GitCode are synced by a mirror workflow.
402
+ - Changelog: [CHANGELOG.md](CHANGELOG.md); questions and reports: [Issues](https://github.com/PerryLink/dsh-locator/issues).
271
403
 
272
404
  ## Roadmap
273
405
 
274
- 1. Electron desktop runtime check on a real Windows install.
406
+ Not a commitment, just the current direction:
407
+
408
+ 1. Verify the Electron desktop runtime on a real Windows install.
275
409
  2. Richer previews and result grouping in the panel.
276
410
  3. Windows on ARM / Linux ARM runtime coverage.
277
- 4. Optional lowercase-name arena and trigram prefilter for large indexes.
411
+ 4. A lowercase-name arena and trigram prefilter for very large indexes.
278
412
  5. Incremental snapshot compaction and per-root journals.
279
413
 
280
- ## Memory
281
-
282
- The engine is a single Go process and honours the standard `GOMEMLIMIT` and `GOGC` environment variables. A 1.24 M-entry index peaks around 700–800 MB RSS with the default GC; set `GOMEMLIMIT=600MiB` (soft limit, the runtime trades CPU for memory) or `GOGC=50` when the host is tight on RAM. Neither changes the snapshot format.
283
-
284
- ## Security and privacy
285
-
286
- The content database and build lock are created `0600` and the state directory `0700`, so other local users cannot read indexed documents or session text.
287
-
288
- The index is local: no network calls, no telemetry, no uploads. The snapshot contains file names and paths, so treat the state directory as private data and exclude it from backups if that matters to you. The plugin never modifies, moves or deletes indexed files.
289
-
290
414
  ## License
291
415
 
292
416
  Apache-2.0.