vde-open 0.1.0 → 0.1.1

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.md CHANGED
@@ -11,21 +11,27 @@ A local document viewer for agents and people who work from the same material. O
11
11
 
12
12
  ## Install
13
13
 
14
- Node.js 24 or later is required (this repository pins 24.21.0 in `mise.toml`).
14
+ Node.js 24 or later is required.
15
15
 
16
16
  ```bash
17
- pnpm install --frozen-lockfile
18
- pnpm build
19
- pnpm test:pack # builds artifacts/vde-open-0.1.0.tgz and verifies an install in a separate directory
20
- bun add -g ./artifacts/vde-open-0.1.0.tgz # recommended: user-level install into ~/.bun/bin
17
+ bun add -g vde-open # recommended: user-level install into ~/.bun/bin
18
+ npm install -g vde-open # also works
21
19
  ```
22
20
 
23
21
  - We recommend installing it once per user with Bun. `~/.bun/bin` does not depend on which Node.js version is active, so switching Node.js versions (with mise and similar tools) does not remove `vo`. Bun is only used to install; the commands run on Node.js (`#!/usr/bin/env node`), so Node.js 24 or later must be on your `PATH`. Running it on the Bun runtime (`bun --bun`) is not tested.
24
- - `npm install -g ./artifacts/vde-open-0.1.0.tgz` also works, but it installs into the prefix of the active Node.js version.
22
+ - `npm install -g` installs into the prefix of the active Node.js version.
25
23
  - Installing it per project is not recommended. There is one daemon per user, so different versions in different projects would talk to the same daemon.
26
- - Always start the path with `./`. Without it, the path is treated as a GitHub repository name.
27
24
 
28
- Installing does not change shell files such as `.zshrc`, and runs no build or install scripts (the tarball bundles every dependency). The package is not published to npm.
25
+ Installing does not change shell files such as `.zshrc`, and runs no build or install scripts (the package bundles every dependency). Releases are published from GitHub Actions with provenance (see "Releasing").
26
+
27
+ To install from a clone of this repository (pins Node.js 24.21.0 in `mise.toml`):
28
+
29
+ ```bash
30
+ pnpm install --frozen-lockfile
31
+ pnpm build
32
+ pnpm test:pack # builds artifacts/vde-open-<version>.tgz and verifies an install in a separate directory
33
+ bun add -g ./artifacts/vde-open-0.1.1.tgz # always start the path with ./ (otherwise it is read as a GitHub repository)
34
+ ```
29
35
 
30
36
  ## `vde-open` and `vo`
31
37
 
@@ -59,8 +65,11 @@ How agents should use it is described in [docs/agent-usage.md](docs/agent-usage.
59
65
  [`skills/vde-open/SKILL.md`](skills/vde-open/SKILL.md) is a skill that teaches an agent (Claude Code, Codex, and others that read `SKILL.md`) when and how to use `vo`. It is also included in the package. To use it, link or copy the directory into your agent's skill directory:
60
66
 
61
67
  ```bash
62
- ln -s "$PWD/skills/vde-open" ~/.claude/skills/vde-open # Claude Code
63
- ln -s "$PWD/skills/vde-open" ~/.codex/skills/vde-open # Codex
68
+ # Installed with Bun: the skill is in the global package directory.
69
+ ln -s ~/.bun/install/global/node_modules/vde-open/skills/vde-open ~/.claude/skills/vde-open # Claude Code
70
+ ln -s ~/.bun/install/global/node_modules/vde-open/skills/vde-open ~/.codex/skills/vde-open # Codex
71
+ # Installed with npm: use "$(npm root -g)/vde-open/skills/vde-open" instead.
72
+ # From a clone of this repository: use "$PWD/skills/vde-open".
64
73
  ```
65
74
 
66
75
  ## Search scope
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { C as decodeStateFile, D as daemonEntryPath, E as resolveStateRoot, O as referencedBlobs, S as renameWithRetry, T as resolveRuntimeLocation, a as DAEMON_LOCK_NAME, b as proofMatches, c as readIpcKey, d as inspectOwner, f as readCurrentLock, g as encodeFrame, h as FrameDecoder, i as requirePrivateDirectoryIfExists, k as version, l as readPointer, m as systemProcessProbe, n as ensurePrivateDirectory, o as POINTER_FILE, p as releaseLock, r as inspectPrivateDirectory, s as START_LOCK_NAME, t as startDaemon, u as acquireLock, v as computeProof, w as currentPathEnvironment, x as removeWithRetry, y as createNonce } from "./main-lxBamyBU.js";
3
- import { A as successEnvelope, C as VdeError, D as ExitCode, E as isVdeError, O as envelopeSchema, T as isErrorCode, Y as LIMITS, k as errorEnvelope, w as exitCodeForError } from "./search-index-C0szMSlZ.js";
2
+ import { C as decodeStateFile, D as daemonEntryPath, E as resolveStateRoot, O as referencedBlobs, S as renameWithRetry, T as resolveRuntimeLocation, a as DAEMON_LOCK_NAME, b as proofMatches, c as readIpcKey, d as inspectOwner, f as readCurrentLock, g as encodeFrame, h as FrameDecoder, i as requirePrivateDirectoryIfExists, k as version, l as readPointer, m as systemProcessProbe, n as ensurePrivateDirectory, o as POINTER_FILE, p as releaseLock, r as inspectPrivateDirectory, s as START_LOCK_NAME, t as startDaemon, u as acquireLock, v as computeProof, w as currentPathEnvironment, x as removeWithRetry, y as createNonce } from "./main-CLzVgxSL.js";
3
+ import { A as successEnvelope, C as VdeError, D as ExitCode, E as isVdeError, O as envelopeSchema, T as isErrorCode, Y as LIMITS, k as errorEnvelope, w as exitCodeForError } from "./search-index-CNsfwB72.js";
4
4
  import fs, { fstatSync } from "node:fs";
5
5
  import { lstat, readFile, readdir, stat as stat$1, writeFile } from "node:fs/promises";
6
6
  import path, { join, resolve } from "node:path";
package/dist/daemon.js CHANGED
@@ -1,5 +1,5 @@
1
- import { k as version, t as startDaemon, w as currentPathEnvironment } from "./main-lxBamyBU.js";
2
- import { E as isVdeError } from "./search-index-C0szMSlZ.js";
1
+ import { k as version, t as startDaemon, w as currentPathEnvironment } from "./main-CLzVgxSL.js";
2
+ import { E as isVdeError } from "./search-index-CNsfwB72.js";
3
3
  //#region src/daemon.ts
4
4
  function notifyParent(message) {
5
5
  if (!process.send) return;
@@ -1,12 +1,17 @@
1
1
  import { getHeapStatistics, setFlagsFromString } from "node:v8";
2
2
  import { runInNewContext } from "node:vm";
3
3
  //#region src/diagnostics/heap.ts
4
- function measureHeap(collectGarbage) {
5
- if (collectGarbage) {
4
+ let gc = null;
5
+ function collectGarbage() {
6
+ if (gc === null) {
6
7
  setFlagsFromString("--expose-gc");
7
- runInNewContext("gc")();
8
+ gc = runInNewContext("gc");
8
9
  }
10
+ gc();
11
+ }
12
+ function measureHeap(collect) {
13
+ if (collect) collectGarbage();
9
14
  return getHeapStatistics().used_heap_size;
10
15
  }
11
16
  //#endregion
12
- export { measureHeap as t };
17
+ export { measureHeap as n, collectGarbage as t };
@@ -1,7 +1,7 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./rolldown-runtime-DC62tzP2.js";
2
- import { $ as literal, A as successEnvelope, B as readParamsSchema, C as VdeError, D as ExitCode, E as isVdeError, F as htmlModeChangeParamsSchema, G as sha256Schema, H as reorderParamsSchema, I as htmlModeSchema, J as watchRuleSchema, K as sourceKindSchema, L as linkOpenParamsSchema, M as closeParamsSchema, N as documentFormatSchema, P as documentIdSchema, Q as boolean, R as listParamsSchema, S as validateAnswers, U as revisionSchema, V as renderGrantParamsSchema, W as searchParamsSchema, X as _enum, Y as LIMITS, Z as array, _ as feedbackWaitParamsSchema, a as uniqueTokens, at as canonicalJson, b as submissionSchema, c as feedbackAckParamsSchema, d as feedbackDraftParamsSchema, et as number, f as feedbackForgetParamsSchema, g as feedbackSubmitParamsSchema, h as feedbackStatusSchema, i as codePointLength, it as ZodError, j as assetRecordSchema, k as errorEnvelope, l as feedbackCancelParamsSchema, m as feedbackListParamsSchema, n as batchesOf, nt as strictObject, o as answersSchema, p as feedbackIdParamsSchema, q as sourceStateSchema, r as partsOf, rt as string, s as cancellationSchema, tt as record, u as feedbackCreateParamsSchema, v as questionnaireSchema, w as exitCodeForError, x as utf8Length, y as requestIdSchema, z as openParamsSchema } from "./search-index-C0szMSlZ.js";
2
+ import { $ as literal, A as successEnvelope, B as readParamsSchema, C as VdeError, D as ExitCode, E as isVdeError, F as htmlModeChangeParamsSchema, G as sha256Schema, H as reorderParamsSchema, I as htmlModeSchema, J as watchRuleSchema, K as sourceKindSchema, L as linkOpenParamsSchema, M as closeParamsSchema, N as documentFormatSchema, P as documentIdSchema, Q as boolean, R as listParamsSchema, S as validateAnswers, U as revisionSchema, V as renderGrantParamsSchema, W as searchParamsSchema, X as _enum, Y as LIMITS, Z as array, _ as feedbackWaitParamsSchema, a as uniqueTokens, at as canonicalJson, b as submissionSchema, c as feedbackAckParamsSchema, d as feedbackDraftParamsSchema, et as number, f as feedbackForgetParamsSchema, g as feedbackSubmitParamsSchema, h as feedbackStatusSchema, i as codePointLength, it as ZodError, j as assetRecordSchema, k as errorEnvelope, l as feedbackCancelParamsSchema, m as feedbackListParamsSchema, n as batchesOf, nt as strictObject, o as answersSchema, p as feedbackIdParamsSchema, q as sourceStateSchema, r as partsOf, rt as string, s as cancellationSchema, tt as record, u as feedbackCreateParamsSchema, v as questionnaireSchema, w as exitCodeForError, x as utf8Length, y as requestIdSchema, z as openParamsSchema } from "./search-index-CNsfwB72.js";
3
3
  import { a as analyzeDocument, c as classifyReference, d as hasHiddenSegment, f as isValidLogicalPath, i as ParseLimitError, l as dirnameOfLogicalPath, n as scanReferences, o as assetTypeOf, p as roleAllowed, r as extractTitle, s as classifyLink, u as encodeLogicalPath } from "./render-yCayIbhf.js";
4
- import { t as measureHeap } from "./heap-Dev_gSQn.js";
4
+ import { n as measureHeap, t as collectGarbage } from "./heap-C3qHm7SE.js";
5
5
  import { constants, existsSync, stat, unwatchFile, watch, watchFile } from "node:fs";
6
6
  import { appendFile, chmod, link, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, rmdir, stat as stat$1, writeFile } from "node:fs/promises";
7
7
  import * as sp from "node:path";
@@ -17,6 +17,8 @@ import { fileURLToPath } from "url";
17
17
  import { createRequire } from "module";
18
18
  import { fileURLToPath as fileURLToPath$1 } from "node:url";
19
19
  import { Worker } from "node:worker_threads";
20
+ import { performance } from "node:perf_hooks";
21
+ import { getHeapStatistics } from "node:v8";
20
22
  import { STATUS_CODES, ServerResponse, createServer } from "node:http";
21
23
  import { Http2ServerRequest, constants as constants$1 } from "node:http2";
22
24
  import { Readable } from "node:stream";
@@ -152,7 +154,7 @@ function parseStrictJson(text) {
152
154
  }
153
155
  //#endregion
154
156
  //#region package.json
155
- var version = "0.1.0";
157
+ var version = "0.1.1";
156
158
  //#endregion
157
159
  //#region src/documents/cursor.ts
158
160
  function invalidCursor(reason) {
@@ -6063,6 +6065,108 @@ function createRenderService(options) {
6063
6065
  };
6064
6066
  }
6065
6067
  //#endregion
6068
+ //#region src/diagnostics/idle-collect.ts
6069
+ const COLLECT_AFTER_IDLE_MS = 2e3;
6070
+ function createIdleTrigger(run, delayMs = COLLECT_AFTER_IDLE_MS) {
6071
+ let timer = null;
6072
+ const cancel = () => {
6073
+ if (timer) clearTimeout(timer);
6074
+ timer = null;
6075
+ };
6076
+ return {
6077
+ touch() {
6078
+ cancel();
6079
+ timer = setTimeout(() => {
6080
+ timer = null;
6081
+ run();
6082
+ }, delayMs);
6083
+ timer.unref();
6084
+ },
6085
+ cancel
6086
+ };
6087
+ }
6088
+ function createWorkerCollect(options) {
6089
+ let collecting = null;
6090
+ let closed = false;
6091
+ const held = [];
6092
+ const release = () => {
6093
+ if (collecting) clearTimeout(collecting.timer);
6094
+ collecting = null;
6095
+ for (const request of held.splice(0)) request.send();
6096
+ };
6097
+ const idle = createIdleTrigger(() => {
6098
+ const worker = options.worker();
6099
+ if (closed || worker === null || collecting) return;
6100
+ if (options.busy()) {
6101
+ idle.touch();
6102
+ return;
6103
+ }
6104
+ const id = options.nextId();
6105
+ const timer = setTimeout(release, options.waitLimitMs ?? 1e4);
6106
+ timer.unref();
6107
+ collecting = {
6108
+ id,
6109
+ timer
6110
+ };
6111
+ worker.postMessage({
6112
+ id,
6113
+ op: "collect"
6114
+ });
6115
+ }, options.delayMs);
6116
+ return {
6117
+ dispatch(send, reject) {
6118
+ if (collecting) held.push({
6119
+ send,
6120
+ reject
6121
+ });
6122
+ else send();
6123
+ },
6124
+ touch: () => idle.touch(),
6125
+ settle(id) {
6126
+ if (collecting?.id !== id) return false;
6127
+ release();
6128
+ return true;
6129
+ },
6130
+ reset: () => {
6131
+ if (collecting) release();
6132
+ },
6133
+ close(error) {
6134
+ closed = true;
6135
+ idle.cancel();
6136
+ if (collecting) clearTimeout(collecting.timer);
6137
+ collecting = null;
6138
+ for (const request of held.splice(0)) request.reject(error);
6139
+ }
6140
+ };
6141
+ }
6142
+ function utilizationSincePreviousCall() {
6143
+ let previous = performance.eventLoopUtilization();
6144
+ return () => {
6145
+ const current = performance.eventLoopUtilization();
6146
+ const { utilization } = performance.eventLoopUtilization(current, previous);
6147
+ previous = current;
6148
+ return utilization;
6149
+ };
6150
+ }
6151
+ function startHeapWatch(options = {}) {
6152
+ const growthBytes = options.growthBytes ?? 8388608;
6153
+ const idleUtilization = options.idleUtilization ?? .05;
6154
+ const heapUsed = options.heapUsed ?? (() => getHeapStatistics().used_heap_size);
6155
+ const utilization = options.utilization ?? utilizationSincePreviousCall();
6156
+ const collect = options.collect ?? collectGarbage;
6157
+ let baseline = heapUsed();
6158
+ const timer = setInterval(() => {
6159
+ const busy = utilization() >= idleUtilization;
6160
+ const used = heapUsed();
6161
+ baseline = Math.min(baseline, used);
6162
+ if (busy || used - baseline < growthBytes) return;
6163
+ collect();
6164
+ baseline = heapUsed();
6165
+ }, options.intervalMs ?? 2e3);
6166
+ timer.unref();
6167
+ return { stop: () => clearInterval(timer) };
6168
+ }
6169
+ //#endregion
6066
6170
  //#region src/diagnostics/until-aborted.ts
6067
6171
  function untilAborted(promise, signal) {
6068
6172
  if (!signal) return promise;
@@ -6148,6 +6252,15 @@ function createSearchService(options) {
6148
6252
  let closed = false;
6149
6253
  let indexedAt = now().toISOString();
6150
6254
  let syncing = null;
6255
+ const collector = createWorkerCollect({
6256
+ worker: () => worker,
6257
+ busy: () => pending.size > 0,
6258
+ nextId: () => {
6259
+ const id = nextId;
6260
+ nextId += 1;
6261
+ return id;
6262
+ }
6263
+ });
6151
6264
  let syncRequested = false;
6152
6265
  const failAll = (error) => {
6153
6266
  for (const waiter of pending.values()) {
@@ -6168,6 +6281,7 @@ function createSearchService(options) {
6168
6281
  const created = new Worker(workerPath);
6169
6282
  created.unref();
6170
6283
  created.on("message", (reply) => {
6284
+ if (collector.settle(reply.id)) return;
6171
6285
  const waiter = pending.get(reply.id);
6172
6286
  if (!waiter) return;
6173
6287
  pending.delete(reply.id);
@@ -6178,31 +6292,36 @@ function createSearchService(options) {
6178
6292
  created.on("error", () => {
6179
6293
  if (worker === created) discardWorker();
6180
6294
  failAll(new VdeError("E_INDEX_NOT_READY", "The search index is not available."));
6295
+ collector.reset();
6181
6296
  });
6182
6297
  worker = created;
6183
6298
  return created;
6184
6299
  };
6185
6300
  const request = (work) => {
6186
6301
  if (closed) return Promise.reject(new VdeError("E_DAEMON_STOPPING", "The daemon is stopping."));
6187
- return new Promise((resolve, reject) => {
6188
- const id = nextId;
6189
- nextId += 1;
6190
- const timer = setTimeout(() => {
6191
- pending.delete(id);
6192
- discardWorker();
6193
- failAll(new VdeError("E_INDEX_NOT_READY", "The search index is not available."));
6194
- reject(new VdeError("E_INDEX_NOT_READY", "The search index operation did not finish in time.", { reason: "timeout" }));
6195
- }, timeoutMs);
6196
- pending.set(id, {
6197
- resolve,
6198
- reject,
6199
- timer
6200
- });
6201
- ensureWorker().postMessage({
6202
- id,
6203
- ...work
6204
- });
6302
+ const result = new Promise((resolve, reject) => {
6303
+ collector.dispatch(() => {
6304
+ const id = nextId;
6305
+ nextId += 1;
6306
+ const timer = setTimeout(() => {
6307
+ pending.delete(id);
6308
+ discardWorker();
6309
+ failAll(new VdeError("E_INDEX_NOT_READY", "The search index is not available."));
6310
+ reject(new VdeError("E_INDEX_NOT_READY", "The search index operation did not finish in time.", { reason: "timeout" }));
6311
+ }, timeoutMs);
6312
+ pending.set(id, {
6313
+ resolve,
6314
+ reject,
6315
+ timer
6316
+ });
6317
+ ensureWorker().postMessage({
6318
+ id,
6319
+ ...work
6320
+ });
6321
+ }, reject);
6205
6322
  });
6323
+ if (work.op !== "diagnostics") result.then(collector.touch, collector.touch);
6324
+ return result;
6206
6325
  };
6207
6326
  const analyzeOnce = async (format, text) => {
6208
6327
  try {
@@ -6513,6 +6632,7 @@ function createSearchService(options) {
6513
6632
  },
6514
6633
  async close() {
6515
6634
  closed = true;
6635
+ collector.close(new VdeError("E_DAEMON_STOPPING", "The daemon is stopping."));
6516
6636
  failAll(new VdeError("E_DAEMON_STOPPING", "The daemon is stopping."));
6517
6637
  const current = worker;
6518
6638
  worker = null;
@@ -12505,6 +12625,15 @@ function createParseService(options = {}) {
12505
12625
  let nextId = 1;
12506
12626
  let closed = false;
12507
12627
  const pending = /* @__PURE__ */ new Map();
12628
+ const collector = createWorkerCollect({
12629
+ worker: () => worker,
12630
+ busy: () => pending.size > 0,
12631
+ nextId: () => {
12632
+ const id = nextId;
12633
+ nextId += 1;
12634
+ return id;
12635
+ }
12636
+ });
12508
12637
  const failAll = (error) => {
12509
12638
  for (const waiter of pending.values()) {
12510
12639
  clearTimeout(waiter.timer);
@@ -12522,6 +12651,7 @@ function createParseService(options = {}) {
12522
12651
  const created = new Worker(workerPath);
12523
12652
  created.unref();
12524
12653
  created.on("message", (reply) => {
12654
+ if (collector.settle(reply.id)) return;
12525
12655
  const waiter = pending.get(reply.id);
12526
12656
  if (!waiter) return;
12527
12657
  pending.delete(reply.id);
@@ -12532,32 +12662,37 @@ function createParseService(options = {}) {
12532
12662
  created.on("error", () => {
12533
12663
  if (worker === created) worker = null;
12534
12664
  failAll(new VdeError("E_PARSE_FAILED", "The document could not be parsed.", { reason: "worker" }));
12665
+ collector.reset();
12535
12666
  });
12536
12667
  worker = created;
12537
12668
  return created;
12538
12669
  };
12539
12670
  const request = (work) => {
12540
12671
  if (closed) return Promise.reject(new VdeError("E_DAEMON_STOPPING", "The daemon is stopping."));
12541
- return new Promise((resolve, reject) => {
12542
- const id = nextId;
12543
- nextId += 1;
12544
- const timer = setTimeout(() => {
12545
- discardWorker();
12546
- failAll(new VdeError("E_PARSE_FAILED", "Parsing the document did not finish in time.", {
12547
- reason: "timeout",
12548
- timeoutMs
12549
- }));
12550
- }, timeoutMs);
12551
- pending.set(id, {
12552
- resolve,
12553
- reject,
12554
- timer
12555
- });
12556
- ensureWorker().postMessage({
12557
- id,
12558
- ...work
12559
- });
12672
+ const result = new Promise((resolve, reject) => {
12673
+ collector.dispatch(() => {
12674
+ const id = nextId;
12675
+ nextId += 1;
12676
+ const timer = setTimeout(() => {
12677
+ discardWorker();
12678
+ failAll(new VdeError("E_PARSE_FAILED", "Parsing the document did not finish in time.", {
12679
+ reason: "timeout",
12680
+ timeoutMs
12681
+ }));
12682
+ }, timeoutMs);
12683
+ pending.set(id, {
12684
+ resolve,
12685
+ reject,
12686
+ timer
12687
+ });
12688
+ ensureWorker().postMessage({
12689
+ id,
12690
+ ...work
12691
+ });
12692
+ }, reject);
12560
12693
  });
12694
+ if (work.op !== "diagnostics") result.then(collector.touch, collector.touch);
12695
+ return result;
12561
12696
  };
12562
12697
  return {
12563
12698
  diagnostics: (collectGarbage, signal) => worker === null ? Promise.resolve(null) : untilAborted(request({
@@ -12580,6 +12715,7 @@ function createParseService(options = {}) {
12580
12715
  }),
12581
12716
  async close() {
12582
12717
  closed = true;
12718
+ collector.close(new VdeError("E_DAEMON_STOPPING", "The daemon is stopping."));
12583
12719
  failAll(new VdeError("E_DAEMON_STOPPING", "The daemon is stopping."));
12584
12720
  const current = worker;
12585
12721
  worker = null;
@@ -12944,6 +13080,7 @@ async function startDaemon(options) {
12944
13080
  let feedback = null;
12945
13081
  let announceStopping = () => void 0;
12946
13082
  let lockTimer = null;
13083
+ let heapWatch = null;
12947
13084
  let stopping = null;
12948
13085
  const stopRequested = new AbortController();
12949
13086
  const owned = {
@@ -12977,6 +13114,7 @@ async function startDaemon(options) {
12977
13114
  };
12978
13115
  stopRequested.abort();
12979
13116
  if (lockTimer) clearInterval(lockTimer);
13117
+ heapWatch?.stop();
12980
13118
  await step("watcher", () => watcher?.close(), SHUTDOWN_CLOSE_TIMEOUT_MS);
12981
13119
  await step("feedback", () => feedback?.close());
12982
13120
  await step("drain", () => ipc?.drain());
@@ -13238,6 +13376,7 @@ async function startDaemon(options) {
13238
13376
  };
13239
13377
  lockTimer = setInterval(() => void checkLock(), LOCK_CHECK_INTERVAL_MS);
13240
13378
  lockTimer.unref();
13379
+ heapWatch = startHeapWatch();
13241
13380
  logger.log("daemon.started", { openDocuments: openedStore.payload.openOrder.length });
13242
13381
  } catch (error) {
13243
13382
  logger.log("daemon.start-failed", { code: error instanceof VdeError ? error.code : "E_INTERNAL" });
@@ -8454,6 +8454,10 @@ function excerptOf(text, normalized, needles) {
8454
8454
  return sliceCodePoints(text.slice(start, start + LIMITS.searchExcerptCodePoints * 2 + EXCERPT_LEAD), 0, LIMITS.searchExcerptCodePoints).replace(/\s+/g, " ").trim();
8455
8455
  }
8456
8456
  const sectionKeyOf = (entry) => `${entry.documentId}\n${String(entry.sectionIndex)}`;
8457
+ function matchedInBody(match) {
8458
+ for (const term in match) if (match[term]?.includes("body") === true) return true;
8459
+ return false;
8460
+ }
8457
8461
  function emptyMatch(stored, sectionIndex) {
8458
8462
  return {
8459
8463
  stored,
@@ -8658,11 +8662,24 @@ var SearchIndex = class {
8658
8662
  }
8659
8663
  };
8660
8664
  if (input.mode === "text" && terms.length > 0) {
8665
+ const restricted = input.documents !== null || this.#staging.size > 0;
8661
8666
  const run = (kind, options) => {
8667
+ const decided = /* @__PURE__ */ new Map();
8668
+ for (const candidate of candidates.values()) {
8669
+ const sections = decided.get(candidate.stored) ?? /* @__PURE__ */ new Set();
8670
+ sections.add(candidate.entry.sectionIndex);
8671
+ decided.set(candidate.stored, sections);
8672
+ }
8673
+ const boostDocument = restricted || decided.size > 0 ? (id) => {
8674
+ const found = this.#entries.get(id);
8675
+ if (!found || !usable(found.stored)) return 0;
8676
+ return decided.get(found.stored)?.has(found.entry.sectionIndex) === true ? 0 : 1;
8677
+ } : void 0;
8662
8678
  let matched = null;
8663
8679
  for (const term of terms) {
8664
8680
  const results = this.#mini.search(term, {
8665
8681
  boost: BOOST,
8682
+ ...boostDocument ? { boostDocument } : {},
8666
8683
  prefix: options.prefix ? (candidate) => candidate.length >= PREFIX_MIN_LENGTH : false,
8667
8684
  fuzzy: options.fuzzy ? (candidate) => allowsFuzzy(candidate) ? FUZZY_DISTANCE : false : false,
8668
8685
  tokenize: (text) => [text],
@@ -8674,14 +8691,13 @@ var SearchIndex = class {
8674
8691
  if (!found || !usable(found.stored)) continue;
8675
8692
  const { stored, entry } = found;
8676
8693
  const key = sectionKeyOf(entry);
8677
- const fields = new Set(Object.values(result.match).flat());
8678
8694
  const current = satisfied.get(key) ?? emptyMatch(stored, entry.sectionIndex);
8679
8695
  current.score = Math.max(current.score, result.score);
8680
8696
  if (result.score > current.entryScore) {
8681
8697
  current.entry = entry;
8682
8698
  current.entryScore = result.score;
8683
8699
  }
8684
- if (fields.has("body") && result.score > current.bodyScore) {
8700
+ if (result.score > current.bodyScore && matchedInBody(result.match)) {
8685
8701
  current.bodyEntry = entry;
8686
8702
  current.bodyScore = result.score;
8687
8703
  }
@@ -8723,7 +8739,7 @@ var SearchIndex = class {
8723
8739
  value.bodyScore = found.bodyScore;
8724
8740
  }
8725
8741
  }
8726
- if (matched.size === 0) return;
8742
+ if (matched.size === 0 && decided.size === 0) return;
8727
8743
  }
8728
8744
  for (const [key, value] of matched ?? []) {
8729
8745
  if (candidates.has(key)) continue;
@@ -8775,7 +8791,16 @@ var SearchIndex = class {
8775
8791
  const shape = stored.sections.get(entry.sectionIndex);
8776
8792
  if (!shape) continue;
8777
8793
  perDocument.set(meta.documentId, count + 1);
8778
- const body = entry.body !== "" ? entry.body : entry.heading || shape.title || meta.title;
8794
+ let body = entry.body;
8795
+ let normalized = entry.normalizedBody;
8796
+ if (body === "") {
8797
+ body = entry.heading;
8798
+ normalized = entry.normalizedHeading;
8799
+ }
8800
+ if (body === "") {
8801
+ body = shape.title || meta.title;
8802
+ normalized = normalizeForSearch(body);
8803
+ }
8779
8804
  hits.push({
8780
8805
  documentId: meta.documentId,
8781
8806
  revision: meta.revision,
@@ -8783,7 +8808,7 @@ var SearchIndex = class {
8783
8808
  displayPath: meta.displayPath,
8784
8809
  sectionId: shape.sectionId,
8785
8810
  headingPath: this.#headingPathOf(stored, entry.sectionIndex),
8786
- excerpt: excerptOf(body, normalizeForSearch(body), [query, ...terms]),
8811
+ excerpt: excerptOf(body, normalized, [query, ...terms]),
8787
8812
  matchKind: kind,
8788
8813
  score: Math.round(score * 1e3) / 1e3,
8789
8814
  sourceRange: null,
@@ -1,5 +1,5 @@
1
1
  import { a as analyzeDocument, i as ParseLimitError, n as scanReferences, t as renderDocument } from "../render-yCayIbhf.js";
2
- import { t as measureHeap } from "../heap-Dev_gSQn.js";
2
+ import { n as measureHeap, t as collectGarbage } from "../heap-C3qHm7SE.js";
3
3
  import { parentPort } from "node:worker_threads";
4
4
  //#region src/workers/parse-worker.ts
5
5
  function run(request) {
@@ -7,6 +7,10 @@ function run(request) {
7
7
  heapUsedBytes: measureHeap(request.collectGarbage),
8
8
  retained: {}
9
9
  };
10
+ if (request.op === "collect") {
11
+ collectGarbage();
12
+ return null;
13
+ }
10
14
  if (request.op === "analyze") return analyzeDocument(request.text, request.format);
11
15
  if (request.op === "scan") return scanReferences(request.kind, request.text);
12
16
  return renderDocument(request.input);
@@ -1,18 +1,21 @@
1
- import { t as SearchIndex } from "../search-index-C0szMSlZ.js";
2
- import { t as measureHeap } from "../heap-Dev_gSQn.js";
1
+ import { t as SearchIndex } from "../search-index-CNsfwB72.js";
2
+ import { n as measureHeap, t as collectGarbage } from "../heap-C3qHm7SE.js";
3
3
  import { parentPort } from "node:worker_threads";
4
4
  //#region src/workers/search-worker.ts
5
5
  const index = new SearchIndex();
6
- async function diagnostics(collectGarbage) {
7
- const retained = await index.retainedCounts(collectGarbage);
6
+ async function diagnostics(collect) {
7
+ const retained = await index.retainedCounts(collect);
8
8
  return {
9
- heapUsedBytes: measureHeap(collectGarbage),
9
+ heapUsedBytes: measureHeap(collect),
10
10
  retained
11
11
  };
12
12
  }
13
13
  function run(request) {
14
14
  switch (request.op) {
15
15
  case "diagnostics": return diagnostics(request.collectGarbage);
16
+ case "collect":
17
+ collectGarbage();
18
+ return null;
16
19
  case "begin":
17
20
  index.begin(request.meta);
18
21
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vde-open",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A local document viewer shared by agents and people: open Markdown and HTML documents, search and read them from the CLI, and ask the person questions.",
5
5
  "keywords": [
6
6
  "agent",
@@ -10,7 +10,7 @@ vde-open is a local document viewer shared by a person and agents. The person re
10
10
  ## Before you start
11
11
 
12
12
  - Check that the CLI exists: `command -v vo || command -v vde-open`. Both names are the same CLI; if `vo` belongs to another tool on this machine, use `vde-open`.
13
- - If neither exists, do not install it on your own. Tell the user it is not installed and point them to the install steps (Node.js 24 or later; from a clone of the repository: `pnpm install --frozen-lockfile && pnpm build && pnpm test:pack && bun add -g ./artifacts/vde-open-0.1.0.tgz`).
13
+ - If neither exists, do not install it on your own. Tell the user it is not installed and point them to the install steps (Node.js 24 or later; `bun add -g vde-open` or `npm install -g vde-open`).
14
14
  - The daemon starts automatically on the first command. You do not need to start it.
15
15
 
16
16
  ## Rules