eminiar 0.10.0 → 0.10.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eminiar",
3
3
  "displayName": "Eminiar",
4
- "version": "0.10.0",
4
+ "version": "0.10.1",
5
5
  "description": "Local-first context, memory and knowledge graph for Claude Code: sandboxed execution, FTS5 + vector search, session continuity, code graph.",
6
6
  "author": {
7
7
  "name": "Ryan Dick"
package/dist/cli.js CHANGED
@@ -21270,6 +21270,14 @@ function findOnPath(bin) {
21270
21270
  }
21271
21271
  return void 0;
21272
21272
  }
21273
+ function requirePreamble(code) {
21274
+ if (!/\brequire\s*\(/.test(code)) return void 0;
21275
+ if (/\b(?:const|let|var|function)\s+require\b/.test(code) || /\bcreateRequire\b/.test(code)) {
21276
+ return void 0;
21277
+ }
21278
+ return `${REQUIRE_PREAMBLE}
21279
+ `;
21280
+ }
21273
21281
  function preferredShell() {
21274
21282
  const sh = process.env.SHELL ?? "";
21275
21283
  if (/\/(zsh|bash)$/.test(sh) && existsSync5(sh)) return sh;
@@ -21290,13 +21298,14 @@ function availableLanguages() {
21290
21298
  return { id: l.id, bin };
21291
21299
  });
21292
21300
  }
21293
- var require3, tsxPath, LANGUAGES;
21301
+ var require3, REQUIRE_PREAMBLE, tsxPath, LANGUAGES;
21294
21302
  var init_languages = __esm({
21295
21303
  "src/sandbox/languages.ts"() {
21296
21304
  "use strict";
21297
21305
  init_esm_shims();
21298
21306
  init_paths();
21299
21307
  require3 = createRequire2(import.meta.url);
21308
+ REQUIRE_PREAMBLE = 'import { createRequire } from "node:module"; const require = createRequire(process.env.EMINIAR_PROJECT_ROOT ? process.env.EMINIAR_PROJECT_ROOT + "/package.json" : import.meta.url);';
21300
21309
  LANGUAGES = [
21301
21310
  {
21302
21311
  id: "shell",
@@ -21310,14 +21319,16 @@ var init_languages = __esm({
21310
21319
  aliases: ["js", "node", "mjs"],
21311
21320
  ext: ".mjs",
21312
21321
  detect: () => process.execPath,
21313
- argv: (bin, file3) => [bin, "--no-warnings", file3]
21322
+ argv: (bin, file3) => [bin, "--no-warnings", file3],
21323
+ preamble: requirePreamble
21314
21324
  },
21315
21325
  {
21316
21326
  id: "typescript",
21317
21327
  aliases: ["ts"],
21318
21328
  ext: ".ts",
21319
21329
  detect: () => process.execPath,
21320
- argv: (bin, file3) => [bin, "--no-warnings", tsxCli(), file3]
21330
+ argv: (bin, file3) => [bin, "--no-warnings", tsxCli(), file3],
21331
+ preamble: requirePreamble
21321
21332
  },
21322
21333
  {
21323
21334
  id: "python",
@@ -21524,6 +21535,10 @@ var init_terms = __esm({
21524
21535
  });
21525
21536
 
21526
21537
  // src/storage/sqlite/schema.ts
21538
+ function hasColumn(db, table, column) {
21539
+ const cols = db.pragma(`table_info(${table})`);
21540
+ return cols.some((c) => c.name === column);
21541
+ }
21527
21542
  function vecTableSql(dims) {
21528
21543
  return `CREATE VIRTUAL TABLE IF NOT EXISTS chunk_vec USING vec0(chunk_id INTEGER PRIMARY KEY, embedding float[${dims}])`;
21529
21544
  }
@@ -21866,7 +21881,6 @@ CREATE TABLE IF NOT EXISTS transcript_files (
21866
21881
  offset INTEGER NOT NULL DEFAULT 0,
21867
21882
  size INTEGER NOT NULL DEFAULT 0,
21868
21883
  mtime INTEGER NOT NULL DEFAULT 0,
21869
- head_hash TEXT,
21870
21884
  ingested_at INTEGER NOT NULL
21871
21885
  );
21872
21886
 
@@ -21894,6 +21908,16 @@ CREATE TABLE IF NOT EXISTS statusline_snapshots (
21894
21908
  CREATE INDEX IF NOT EXISTS statusline_snapshots_session_ts ON statusline_snapshots(session_id, ts);
21895
21909
  CREATE INDEX IF NOT EXISTS statusline_snapshots_ts ON statusline_snapshots(ts);
21896
21910
  `
21911
+ },
21912
+ {
21913
+ // transcript_files.head_hash (fingerprint of the file head, so a rewrite with the same
21914
+ // size is noticed) was first added to v5 after v5 had already run on real databases, so
21915
+ // version-5 databases exist both with and without the column. Add it only when missing.
21916
+ version: 6,
21917
+ up(db) {
21918
+ if (!hasColumn(db, "transcript_files", "head_hash"))
21919
+ db.exec("ALTER TABLE transcript_files ADD COLUMN head_hash TEXT");
21920
+ }
21897
21921
  }
21898
21922
  ];
21899
21923
  }
@@ -22013,7 +22037,8 @@ var init_adapter = __esm({
22013
22037
  if (pending2.length) {
22014
22038
  this.transaction(() => {
22015
22039
  for (const m of pending2) {
22016
- this.db.exec(m.sql);
22040
+ if (m.sql) this.db.exec(m.sql);
22041
+ m.up?.(this.db);
22017
22042
  this.metaSet("schema_version", String(m.version));
22018
22043
  }
22019
22044
  });
@@ -22232,18 +22257,42 @@ var init_adapter = __esm({
22232
22257
  }
22233
22258
  vecSearch(embedding, k, filters) {
22234
22259
  if (!this.vectors) return [];
22235
- const raw = this.db.prepare(
22236
- "SELECT chunk_id id, distance FROM chunk_vec WHERE embedding MATCH ? AND k = ? ORDER BY distance"
22237
- ).all(embedding, BigInt(filters ? k * 4 : k));
22238
- if (!filters || !raw.length) return raw.map((r) => ({ chunkId: r.id, distance: r.distance }));
22239
22260
  const { where, params } = filterSql(filters);
22240
- const allowed = new Set(
22241
- this.db.prepare(
22242
- `SELECT c.id FROM chunks c JOIN documents d ON d.id = c.document_id
22243
- WHERE c.id IN (${raw.map(() => "?").join(",")})${where}`
22244
- ).all(...raw.map((r) => r.id), ...params).map((r) => r.id)
22245
- );
22246
- return raw.filter((r) => allowed.has(r.id)).slice(0, k).map((r) => ({ chunkId: r.id, distance: r.distance }));
22261
+ if (!where) return this.knn(embedding, k);
22262
+ try {
22263
+ return this.knn(
22264
+ embedding,
22265
+ k,
22266
+ `AND chunk_id IN (SELECT c.id FROM chunks c JOIN documents d ON d.id = c.document_id WHERE 1=1${where})`,
22267
+ params
22268
+ );
22269
+ } catch {
22270
+ return this.vecSearchWidening(embedding, k, where, params);
22271
+ }
22272
+ }
22273
+ knn(embedding, k, extra = "", params = []) {
22274
+ return this.db.prepare(
22275
+ `SELECT chunk_id id, distance FROM chunk_vec WHERE embedding MATCH ? AND k = ? ${extra} ORDER BY distance`
22276
+ ).all(embedding, BigInt(k), ...params).map((r) => ({ chunkId: r.id, distance: r.distance }));
22277
+ }
22278
+ /** Fallback filtered KNN: fetch k*4, k*16, ... nearest until k pass the filter or all were seen. */
22279
+ vecSearchWidening(embedding, k, where, params) {
22280
+ const total = this.vecCount();
22281
+ for (let fetch2 = k * 4; ; fetch2 *= 4) {
22282
+ const raw = this.knn(embedding, Math.min(fetch2, total));
22283
+ if (!raw.length) return [];
22284
+ const allowed = /* @__PURE__ */ new Set();
22285
+ for (let i = 0; i < raw.length; i += 400) {
22286
+ const ids = raw.slice(i, i + 400).map((r) => r.chunkId);
22287
+ for (const r of this.db.prepare(
22288
+ `SELECT c.id FROM chunks c JOIN documents d ON d.id = c.document_id
22289
+ WHERE c.id IN (${ids.map(() => "?").join(",")})${where}`
22290
+ ).all(...ids, ...params))
22291
+ allowed.add(r.id);
22292
+ }
22293
+ const hits = raw.filter((r) => allowed.has(r.chunkId)).slice(0, k);
22294
+ if (hits.length >= k || fetch2 >= total) return hits;
22295
+ }
22247
22296
  }
22248
22297
  unembeddedChunkIds(limit) {
22249
22298
  return this.db.prepare("SELECT id FROM chunks WHERE embedded = 0 ORDER BY id LIMIT ?").all(limit).map((r) => r.id);
@@ -22522,6 +22571,26 @@ var init_adapter = __esm({
22522
22571
  }
22523
22572
  return out;
22524
22573
  }
22574
+ graphDependentFileCounts(paths2, kinds) {
22575
+ const out = /* @__PURE__ */ new Map();
22576
+ if (!paths2.length || !kinds.length) return out;
22577
+ const kph = kinds.map(() => "?").join(",");
22578
+ for (let i = 0; i < paths2.length; i += 400) {
22579
+ const batch = paths2.slice(i, i + 400);
22580
+ const pph = batch.map(() => "?").join(",");
22581
+ for (const r of this.db.prepare(
22582
+ `SELECT d.path p, COUNT(DISTINCT s.path) n
22583
+ FROM graph_nodes d
22584
+ JOIN graph_edges e ON e.dst = d.id AND e.kind IN (${kph})
22585
+ JOIN graph_nodes s ON s.id = e.src
22586
+ WHERE d.path IN (${pph}) AND s.path IS NOT NULL AND s.path <> d.path
22587
+ GROUP BY d.path`
22588
+ ).all(...kinds, ...batch)) {
22589
+ out.set(r.p, r.n);
22590
+ }
22591
+ }
22592
+ return out;
22593
+ }
22525
22594
  edgesWhere(col, ids, kinds) {
22526
22595
  if (!ids.length) return [];
22527
22596
  const out = [];
@@ -30413,7 +30482,7 @@ function chunkMarkdown(text3, baseTitle, opts = DEFAULT_CHUNKER) {
30413
30482
  if (/^\s*(```|~~~)/.test(line)) inFence = !inFence;
30414
30483
  const m = !inFence && /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
30415
30484
  if (m) {
30416
- if (current.lines.length) sections.push(current);
30485
+ sections.push(current);
30417
30486
  const level = m[1].length;
30418
30487
  while (stack.length && stack[stack.length - 1].level >= level) stack.pop();
30419
30488
  stack.push({ level, text: m[2] });
@@ -30422,14 +30491,23 @@ function chunkMarkdown(text3, baseTitle, opts = DEFAULT_CHUNKER) {
30422
30491
  }
30423
30492
  current.lines.push(line);
30424
30493
  });
30425
- if (current.lines.length) sections.push(current);
30494
+ sections.push(current);
30426
30495
  const out = [];
30427
- for (const s of sections) {
30496
+ sections.forEach((s, k) => {
30428
30497
  const title = s.path.length ? `${baseTitle} > ${s.path.join(" > ")}` : baseTitle;
30429
30498
  const body = s.lines.join("\n");
30499
+ if (!body.trim()) {
30500
+ const next = sections[k + 1];
30501
+ const isParent = next !== void 0 && next.path.length > s.path.length && s.path.every((p, j) => next.path[j] === p);
30502
+ if (s.path.length && !isParent) {
30503
+ const heading = s.path[s.path.length - 1];
30504
+ out.push({ title, body: heading, startLine: s.startLine, endLine: s.startLine });
30505
+ }
30506
+ return;
30507
+ }
30430
30508
  if (body.length <= opts.maxChars) {
30431
30509
  out.push({ title, body, startLine: s.startLine, endLine: s.startLine + s.lines.length - 1 });
30432
- continue;
30510
+ return;
30433
30511
  }
30434
30512
  let fence = false;
30435
30513
  const fenceState = s.lines.map((l) => {
@@ -30445,7 +30523,7 @@ function chunkMarkdown(text3, baseTitle, opts = DEFAULT_CHUNKER) {
30445
30523
  s.startLine - 1
30446
30524
  )
30447
30525
  );
30448
- }
30526
+ });
30449
30527
  return out;
30450
30528
  }
30451
30529
  var init_markdown = __esm({
@@ -30557,7 +30635,20 @@ import { readFileSync as readFileSync10, statSync } from "fs";
30557
30635
  import { relative, resolve as resolve2 } from "path";
30558
30636
  function indexContent(db, input2) {
30559
30637
  const kind = input2.kind ?? (input2.contentType === "code" ? "code" : input2.contentType === "json" ? "json" : void 0);
30560
- const { chunks, kind: k } = input2.chunks ? { chunks: finalize2(input2.chunks, input2.contentType ?? "code"), kind: kind ?? "code" } : chunk(input2.content, input2.title, kind, input2.uri);
30638
+ const cut = input2.chunks ? { chunks: finalize2(input2.chunks, input2.contentType ?? "code"), kind: kind ?? "code" } : chunk(input2.content, input2.title, kind, input2.uri);
30639
+ const k = cut.kind;
30640
+ const fallback = cut.chunks.length === 0 && input2.content.trim().length > 0;
30641
+ const chunks = fallback ? finalize2(
30642
+ [
30643
+ {
30644
+ title: input2.title,
30645
+ body: input2.content,
30646
+ startLine: 1,
30647
+ endLine: input2.content.split("\n").length
30648
+ }
30649
+ ],
30650
+ kindToContentType(k)
30651
+ ) : cut.chunks;
30561
30652
  const contentHash = hashText(input2.content);
30562
30653
  const existing = input2.uri ? db.findDocumentByUri(input2.uri) : void 0;
30563
30654
  if (existing && existing.content_hash === contentHash) {
@@ -30567,7 +30658,8 @@ function indexContent(db, input2) {
30567
30658
  created: false,
30568
30659
  chunks: chunks.length,
30569
30660
  chars: input2.content.length,
30570
- kind: k
30661
+ kind: k,
30662
+ fallback
30571
30663
  };
30572
30664
  }
30573
30665
  db.deleteDocument(existing.id);
@@ -30584,11 +30676,25 @@ function indexContent(db, input2) {
30584
30676
  },
30585
30677
  chunks
30586
30678
  );
30587
- return { documentId: id, created, chunks: chunks.length, chars: input2.content.length, kind: k };
30679
+ return {
30680
+ documentId: id,
30681
+ created,
30682
+ chunks: chunks.length,
30683
+ chars: input2.content.length,
30684
+ kind: k,
30685
+ fallback
30686
+ };
30588
30687
  }
30589
30688
  function indexPath(db, root, target, opts = {}) {
30590
30689
  const abs = resolve2(root, target);
30591
- const summary = { files: 0, indexed: 0, unchanged: 0, skipped: [], chunks: 0 };
30690
+ const summary = {
30691
+ files: 0,
30692
+ indexed: 0,
30693
+ unchanged: 0,
30694
+ skipped: [],
30695
+ chunks: 0,
30696
+ fallbacks: 0
30697
+ };
30592
30698
  const st = statSync(abs);
30593
30699
  const files = st.isDirectory() ? import_fast_glob.default.sync(opts.globs ?? ["**/*.md"], {
30594
30700
  cwd: abs,
@@ -30623,6 +30729,7 @@ function indexPath(db, root, target, opts = {}) {
30623
30729
  if (r.created) summary.indexed++;
30624
30730
  else summary.unchanged++;
30625
30731
  summary.chunks += r.chunks;
30732
+ if (r.fallback) summary.fallbacks++;
30626
30733
  } catch (err) {
30627
30734
  summary.skipped.push({ path: rel, reason: err.message });
30628
30735
  }
@@ -30698,7 +30805,7 @@ var init_em_index = __esm({
30698
30805
  });
30699
30806
  kickEmbedding(ctx, db);
30700
30807
  return text(
30701
- `${r.created ? "indexed" : "unchanged"} "${args.title}" as doc ${r.documentId}: ${r.chunks} chunks, ${r.chars} chars (${r.kind})`
30808
+ `${r.created ? "indexed" : "unchanged"} "${args.title}" as doc ${r.documentId}: ${r.chunks} chunks, ${r.chars} chars (${r.kind})${r.fallback ? " (no sections found; indexed as one chunk)" : ""}`
30702
30809
  );
30703
30810
  }
30704
30811
  if (!args.path) return errorResult("provide `path` or `content`");
@@ -30708,7 +30815,7 @@ var init_em_index = __esm({
30708
30815
  recursive: args.recursive ?? true
30709
30816
  });
30710
30817
  const lines = [
30711
- `${args.path}: ${s.files} file(s), ${s.indexed} indexed, ${s.unchanged} unchanged, ${s.chunks} chunks`
30818
+ `${args.path}: ${s.files} file(s), ${s.indexed} indexed, ${s.unchanged} unchanged, ${s.chunks} chunks${s.fallbacks ? ` (${s.fallbacks} with no sections found; indexed as one chunk)` : ""}`
30712
30819
  ];
30713
30820
  for (const sk of s.skipped.slice(0, 10)) lines.push(` skipped ${sk.path}: ${sk.reason}`);
30714
30821
  if (s.skipped.length > 10) lines.push(` \u2026 ${s.skipped.length - 10} more skipped`);
@@ -38324,6 +38431,9 @@ import { dirname as dirname9, join as join24 } from "path";
38324
38431
  function docsTag(name) {
38325
38432
  return `${DOCS_PREFIX}${name}`;
38326
38433
  }
38434
+ function nameFromTag(tag) {
38435
+ return tag.startsWith(DOCS_PREFIX) ? tag.slice(DOCS_PREFIX.length) : void 0;
38436
+ }
38327
38437
  function isValidName2(name) {
38328
38438
  return NAME_RE2.test(name);
38329
38439
  }
@@ -40848,7 +40958,7 @@ function removeDocSource(ctx, db, name) {
40848
40958
  const removed = removeSource(ctx, name);
40849
40959
  const documents = db.deleteDocumentsBySource(docsTag(name));
40850
40960
  clearSyncState(db, name);
40851
- if (removed) syncDocsGraphQuietly(ctx, db);
40961
+ if (removed || documents) syncDocsGraphQuietly(ctx, db);
40852
40962
  return { removed, documents };
40853
40963
  }
40854
40964
  function formatAdd(o) {
@@ -40903,7 +41013,8 @@ var init_add = __esm({
40903
41013
  var em_docs_exports = {};
40904
41014
  __export(em_docs_exports, {
40905
41015
  emDocs: () => emDocs,
40906
- formatDiscovery: () => formatDiscovery
41016
+ formatDiscovery: () => formatDiscovery,
41017
+ orphanedDocSources: () => orphanedDocSources
40907
41018
  });
40908
41019
  function toInput(a) {
40909
41020
  return {
@@ -40930,6 +41041,26 @@ function ago(ts) {
40930
41041
  if (s < 172800) return `${Math.round(s / 3600)}h ago`;
40931
41042
  return `${Math.round(s / 86400)}d ago`;
40932
41043
  }
41044
+ function orphanedDocSources(db, registered, stats = db.documentSourceStats(DOCS_PREFIX)) {
41045
+ const known = new Set(registered);
41046
+ const out = [];
41047
+ for (const s of stats) {
41048
+ const name = nameFromTag(s.source);
41049
+ if (name === void 0 || known.has(name)) continue;
41050
+ out.push({ name, documents: s.documents, chunks: s.chunks, latest: s.latest });
41051
+ }
41052
+ return out;
41053
+ }
41054
+ function formatOrphans(orphans2) {
41055
+ const names = orphans2.map((o) => o.name);
41056
+ return [
41057
+ `# Orphaned in the index (${orphans2.length}; pages without a manifest entry)`,
41058
+ ...orphans2.map(
41059
+ (o) => `- ${o.name} \xB7 ${o.documents} doc(s) \xB7 ${o.chunks} chunks \xB7 indexed ${ago(o.latest)}`
41060
+ ),
41061
+ `em_docs {action:"remove", names:${JSON.stringify(names)}} drops them; {action:"add", pick:"recommended"} or {action:"add", name, url|path} re-registers.`
41062
+ ];
41063
+ }
40933
41064
  function formatDiscovery(d, max = 40) {
40934
41065
  const lines = [];
40935
41066
  lines.push(
@@ -41112,19 +41243,30 @@ var init_em_docs = __esm({
41112
41243
  case "remove": {
41113
41244
  const names = [...args.name ? [args.name] : [], ...args.names ?? []];
41114
41245
  if (!names.length) return errorResult("remove needs `name` or `names`");
41246
+ let unknown2 = 0;
41115
41247
  const lines = names.map((n) => {
41116
41248
  const r = removeDocSource(ctx, db, n);
41117
- return r.removed ? `removed ${n} (${r.documents} document(s) dropped)` : `${n}: not registered${r.documents ? ` (${r.documents} orphan document(s) dropped)` : ""}`;
41249
+ if (r.removed) return `removed ${n} (${r.documents} document(s) dropped)`;
41250
+ if (r.documents) return `removed ${n} (orphaned; ${r.documents} document(s) dropped)`;
41251
+ unknown2++;
41252
+ return `${n}: not registered and not in the index; em_docs {action:"list"} shows both`;
41118
41253
  });
41119
- return done(lines.join("\n"));
41254
+ const out = lines.join("\n");
41255
+ return unknown2 === names.length ? errorResult(out) : done(out);
41120
41256
  }
41121
41257
  case "list": {
41122
41258
  const m = readManifest(ctx);
41123
- if (!m.sources.length)
41124
- return done(
41125
- `no documentation sources registered (${manifestPath(ctx)}). em_docs {action:"discover"} finds local docs and the libraries, services and APIs this project uses.`
41126
- );
41127
- const stats = new Map(db.documentSourceStats(DOCS_PREFIX).map((s) => [s.source, s]));
41259
+ const sourceStats = db.documentSourceStats(DOCS_PREFIX);
41260
+ const orphans2 = orphanedDocSources(
41261
+ db,
41262
+ m.sources.map((s) => s.name),
41263
+ sourceStats
41264
+ );
41265
+ if (!m.sources.length) {
41266
+ const none = `no documentation sources registered (${manifestPath(ctx)}). em_docs {action:"discover"} finds local docs and the libraries, services and APIs this project uses.`;
41267
+ return done(orphans2.length ? [none, "", ...formatOrphans(orphans2)].join("\n") : none);
41268
+ }
41269
+ const stats = new Map(sourceStats.map((s) => [s.source, s]));
41128
41270
  const catalog = mergedCatalog(ctx.config.docs.catalog);
41129
41271
  const categorised = m.sources.map((s) => ({
41130
41272
  s,
@@ -41155,9 +41297,10 @@ var init_em_docs = __esm({
41155
41297
  `- ${s.name} [${s.kind}]${s.provider ? ` \u2190 ${s.provider}` : ""}${s.tags?.length ? ` (${s.tags.join(", ")})` : ""} \xB7 ${st?.documents ?? 0} doc(s) \xB7 ${st?.chunks ?? 0} chunks \xB7 ${state} \xB7 ${where}`
41156
41298
  );
41157
41299
  }
41300
+ if (orphans2.length) lines.push("", ...formatOrphans(orphans2));
41158
41301
  lines.push(
41159
41302
  "",
41160
- `${m.sources.length} source(s); em_docs {action:"lookup", query} searches them all, {action:"sync"} refreshes.`
41303
+ `${m.sources.length} source(s)${orphans2.length ? ` + ${orphans2.length} orphaned` : ""}; em_docs {action:"lookup", query} searches them all, {action:"sync"} refreshes.`
41161
41304
  );
41162
41305
  return done(lines.join("\n"));
41163
41306
  }
@@ -41203,10 +41346,18 @@ ${r.urls.map((u) => ` ${u}`).join("\n")}
41203
41346
  raw += r.hits.reduce((n, h) => n + h.chunk.body.length, 0);
41204
41347
  responses.push(r);
41205
41348
  }
41206
- const out = formatResults(
41207
- responses,
41208
- args.max_tokens ?? ctx.config.search.defaultMaxTokens
41349
+ let out = formatResults(responses, args.max_tokens ?? ctx.config.search.defaultMaxTokens);
41350
+ const hitSources = new Set(
41351
+ responses.flatMap((r) => r.hits.map((h) => h.chunk.document.source))
41209
41352
  );
41353
+ const orphaned = orphanedDocSources(
41354
+ db,
41355
+ m.sources.map((s) => s.name)
41356
+ ).filter((o) => hitSources.has(docsTag(o.name)));
41357
+ if (orphaned.length)
41358
+ out += `
41359
+
41360
+ note: ${orphaned.map((o) => o.name).join(", ")} ${orphaned.length === 1 ? "is" : "are"} not in the manifest (orphaned pages); em_docs {action:"list"} explains`;
41210
41361
  recordStat(ctx, "em_docs", {
41211
41362
  rawChars: raw,
41212
41363
  returnedChars: out.length,
@@ -42467,6 +42618,9 @@ function memoriesForFile(db, rel, opts = {}) {
42467
42618
  trace
42468
42619
  };
42469
42620
  }
42621
+ function dependentFiles(db, paths2) {
42622
+ return db.graphDependentFileCounts(paths2, DEPENDENCY_KINDS);
42623
+ }
42470
42624
  function riskLine(path3, f) {
42471
42625
  if (f.risk < RISKY && !f.bugOrigins) return void 0;
42472
42626
  const parts = [`${f.fixes}/${f.commits} commits were fixes`];
@@ -42494,10 +42648,10 @@ function fileContext(db, rel, opts = {}) {
42494
42648
  }
42495
42649
  const fanKey = `fanin:${rel}`;
42496
42650
  if (opts.edit && !seen.has(fanKey)) {
42497
- const fanIn = db.metricsGet([fileId(rel)])[0]?.fan_in ?? 0;
42498
- if (fanIn >= FAN_IN) {
42651
+ const dependents = dependentFiles(db, [rel]).get(rel) ?? 0;
42652
+ if (dependents >= FAN_IN) {
42499
42653
  lines.push(
42500
- `eminiar graph: ${rel} is imported by ${fanIn} files; em_graph_impact {node:"${fileId(rel)}"} lists them before you change an exported signature.`
42654
+ `eminiar graph: ${rel} is imported by ${dependents} files; em_graph_impact {node:"${fileId(rel)}"} lists them before you change an exported signature.`
42501
42655
  );
42502
42656
  keys.push(fanKey);
42503
42657
  }
@@ -42554,6 +42708,7 @@ var init_recall = __esm({
42554
42708
  "use strict";
42555
42709
  init_esm_shims();
42556
42710
  init_ids();
42711
+ init_metrics();
42557
42712
  init_model();
42558
42713
  init_pipeline();
42559
42714
  init_query();
@@ -44546,12 +44701,17 @@ function usageLine(ctx, sessionId) {
44546
44701
  if (!usageEnabled(ctx)) return void 0;
44547
44702
  try {
44548
44703
  const db = getStorage(ctx);
44549
- ingestSession(db, ctx.root, sessionId, void 0, { overrides: ctx.config.usage.pricing });
44704
+ let failed;
44705
+ try {
44706
+ ingestSession(db, ctx.root, sessionId, void 0, { overrides: ctx.config.usage.pricing });
44707
+ } catch (err) {
44708
+ failed = `usage: ingest failed: ${err.message}`;
44709
+ }
44550
44710
  const b = usageBrief(db, ctx, { sessionId });
44551
- if (!b.todayUsd && !b.sessionUsd) return void 0;
44552
- return `usage: today ${formatUsd(b.todayUsd)} \xB7 session ${formatUsd(b.sessionUsd)} \xB7 see em_usage`;
44553
- } catch {
44554
- return void 0;
44711
+ const line = b.todayUsd || b.sessionUsd ? `usage: today ${formatUsd(b.todayUsd)} \xB7 session ${formatUsd(b.sessionUsd)} \xB7 see em_usage` : void 0;
44712
+ return [line, failed].filter((s) => s).join("\n") || void 0;
44713
+ } catch (err) {
44714
+ return `usage: unavailable: ${err.message}`;
44555
44715
  }
44556
44716
  }
44557
44717
  function mergeByTool(rows) {
@@ -45825,7 +45985,10 @@ var init_em_search = __esm({
45825
45985
  }
45826
45986
  }
45827
45987
  const out = formatResults(responses, args.max_tokens ?? ctx.config.search.defaultMaxTokens);
45828
- const note = mode !== "lexical" && mode !== "regex" && !vectorSearch && ctx.config.embeddings.enabled ? "\n\n(embeddings: model still loading \u2014 lexical results only this time)" : "";
45988
+ let note = mode !== "lexical" && mode !== "regex" && !vectorSearch && ctx.config.embeddings.enabled ? "\n\n(embeddings: model still loading \u2014 lexical results only this time)" : "";
45989
+ const sources = Array.isArray(args.source) ? args.source : args.source ? [args.source] : [];
45990
+ if (mode === "semantic" && !ctx.config.docs.embed && sources.some((s) => s.startsWith("docs:")) && responses.every((r) => r.hits.length === 0))
45991
+ note += '\n\n(docs:* pages are indexed without vectors because docs.embed is off; use mode "hybrid" or "lexical", or set docs.embed: true in eminiar.config.json and em_docs {action:"sync", force:true})';
45829
45992
  recordStat(ctx, "em_search", {
45830
45993
  rawChars: responses.reduce(
45831
45994
  (a, r) => a + r.hits.reduce((b, h) => b + h.chunk.body.length, 0),
@@ -49037,7 +49200,8 @@ try {
49037
49200
  } catch (err) {
49038
49201
  console.error(`eminiar ${cmd} failed: ${err.message}`);
49039
49202
  if (process.env.EMINIAR_DEBUG) console.error(err.stack);
49040
- if (cmd !== "doctor") console.error(formatDoctor(runDoctor()));
49203
+ if (cmd === "usage") console.error("run `eminiar doctor` to check the installation");
49204
+ else if (cmd !== "doctor") console.error(formatDoctor(runDoctor()));
49041
49205
  process.exit(1);
49042
49206
  }
49043
49207
  /*! Bundled license information: