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.
package/dist/server.js CHANGED
@@ -13518,7 +13518,7 @@ function chunkMarkdown(text2, baseTitle, opts = DEFAULT_CHUNKER) {
13518
13518
  if (/^\s*(```|~~~)/.test(line)) inFence = !inFence;
13519
13519
  const m = !inFence && /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
13520
13520
  if (m) {
13521
- if (current.lines.length) sections.push(current);
13521
+ sections.push(current);
13522
13522
  const level = m[1].length;
13523
13523
  while (stack.length && stack[stack.length - 1].level >= level) stack.pop();
13524
13524
  stack.push({ level, text: m[2] });
@@ -13527,14 +13527,23 @@ function chunkMarkdown(text2, baseTitle, opts = DEFAULT_CHUNKER) {
13527
13527
  }
13528
13528
  current.lines.push(line);
13529
13529
  });
13530
- if (current.lines.length) sections.push(current);
13530
+ sections.push(current);
13531
13531
  const out = [];
13532
- for (const s of sections) {
13532
+ sections.forEach((s, k) => {
13533
13533
  const title = s.path.length ? `${baseTitle} > ${s.path.join(" > ")}` : baseTitle;
13534
13534
  const body = s.lines.join("\n");
13535
+ if (!body.trim()) {
13536
+ const next = sections[k + 1];
13537
+ const isParent = next !== void 0 && next.path.length > s.path.length && s.path.every((p, j) => next.path[j] === p);
13538
+ if (s.path.length && !isParent) {
13539
+ const heading = s.path[s.path.length - 1];
13540
+ out.push({ title, body: heading, startLine: s.startLine, endLine: s.startLine });
13541
+ }
13542
+ return;
13543
+ }
13535
13544
  if (body.length <= opts.maxChars) {
13536
13545
  out.push({ title, body, startLine: s.startLine, endLine: s.startLine + s.lines.length - 1 });
13537
- continue;
13546
+ return;
13538
13547
  }
13539
13548
  let fence2 = false;
13540
13549
  const fenceState = s.lines.map((l) => {
@@ -13550,7 +13559,7 @@ function chunkMarkdown(text2, baseTitle, opts = DEFAULT_CHUNKER) {
13550
13559
  s.startLine - 1
13551
13560
  )
13552
13561
  );
13553
- }
13562
+ });
13554
13563
  return out;
13555
13564
  }
13556
13565
  var init_markdown = __esm({
@@ -16953,7 +16962,20 @@ import { readFileSync as readFileSync6, statSync as statSync4 } from "fs";
16953
16962
  import { relative as relative5, resolve as resolve6 } from "path";
16954
16963
  function indexContent(db, input2) {
16955
16964
  const kind = input2.kind ?? (input2.contentType === "code" ? "code" : input2.contentType === "json" ? "json" : void 0);
16956
- const { chunks, kind: k } = input2.chunks ? { chunks: finalize2(input2.chunks, input2.contentType ?? "code"), kind: kind ?? "code" } : chunk(input2.content, input2.title, kind, input2.uri);
16965
+ const cut = input2.chunks ? { chunks: finalize2(input2.chunks, input2.contentType ?? "code"), kind: kind ?? "code" } : chunk(input2.content, input2.title, kind, input2.uri);
16966
+ const k = cut.kind;
16967
+ const fallback = cut.chunks.length === 0 && input2.content.trim().length > 0;
16968
+ const chunks = fallback ? finalize2(
16969
+ [
16970
+ {
16971
+ title: input2.title,
16972
+ body: input2.content,
16973
+ startLine: 1,
16974
+ endLine: input2.content.split("\n").length
16975
+ }
16976
+ ],
16977
+ kindToContentType(k)
16978
+ ) : cut.chunks;
16957
16979
  const contentHash = hashText(input2.content);
16958
16980
  const existing = input2.uri ? db.findDocumentByUri(input2.uri) : void 0;
16959
16981
  if (existing && existing.content_hash === contentHash) {
@@ -16963,7 +16985,8 @@ function indexContent(db, input2) {
16963
16985
  created: false,
16964
16986
  chunks: chunks.length,
16965
16987
  chars: input2.content.length,
16966
- kind: k
16988
+ kind: k,
16989
+ fallback
16967
16990
  };
16968
16991
  }
16969
16992
  db.deleteDocument(existing.id);
@@ -16980,11 +17003,25 @@ function indexContent(db, input2) {
16980
17003
  },
16981
17004
  chunks
16982
17005
  );
16983
- return { documentId: id, created, chunks: chunks.length, chars: input2.content.length, kind: k };
17006
+ return {
17007
+ documentId: id,
17008
+ created,
17009
+ chunks: chunks.length,
17010
+ chars: input2.content.length,
17011
+ kind: k,
17012
+ fallback
17013
+ };
16984
17014
  }
16985
17015
  function indexPath(db, root, target, opts = {}) {
16986
17016
  const abs = resolve6(root, target);
16987
- const summary = { files: 0, indexed: 0, unchanged: 0, skipped: [], chunks: 0 };
17017
+ const summary = {
17018
+ files: 0,
17019
+ indexed: 0,
17020
+ unchanged: 0,
17021
+ skipped: [],
17022
+ chunks: 0,
17023
+ fallbacks: 0
17024
+ };
16988
17025
  const st = statSync4(abs);
16989
17026
  const files = st.isDirectory() ? import_fast_glob.default.sync(opts.globs ?? ["**/*.md"], {
16990
17027
  cwd: abs,
@@ -17019,6 +17056,7 @@ function indexPath(db, root, target, opts = {}) {
17019
17056
  if (r.created) summary.indexed++;
17020
17057
  else summary.unchanged++;
17021
17058
  summary.chunks += r.chunks;
17059
+ if (r.fallback) summary.fallbacks++;
17022
17060
  } catch (err) {
17023
17061
  summary.skipped.push({ path: rel, reason: err.message });
17024
17062
  }
@@ -49547,6 +49585,10 @@ function topTermsOf(bodies, limit) {
49547
49585
 
49548
49586
  // src/storage/sqlite/schema.ts
49549
49587
  init_esm_shims();
49588
+ function hasColumn(db, table, column) {
49589
+ const cols = db.pragma(`table_info(${table})`);
49590
+ return cols.some((c) => c.name === column);
49591
+ }
49550
49592
  var migrations = [
49551
49593
  {
49552
49594
  version: 1,
@@ -49881,7 +49923,6 @@ CREATE TABLE IF NOT EXISTS transcript_files (
49881
49923
  offset INTEGER NOT NULL DEFAULT 0,
49882
49924
  size INTEGER NOT NULL DEFAULT 0,
49883
49925
  mtime INTEGER NOT NULL DEFAULT 0,
49884
- head_hash TEXT,
49885
49926
  ingested_at INTEGER NOT NULL
49886
49927
  );
49887
49928
 
@@ -49909,6 +49950,16 @@ CREATE TABLE IF NOT EXISTS statusline_snapshots (
49909
49950
  CREATE INDEX IF NOT EXISTS statusline_snapshots_session_ts ON statusline_snapshots(session_id, ts);
49910
49951
  CREATE INDEX IF NOT EXISTS statusline_snapshots_ts ON statusline_snapshots(ts);
49911
49952
  `
49953
+ },
49954
+ {
49955
+ // transcript_files.head_hash (fingerprint of the file head, so a rewrite with the same
49956
+ // size is noticed) was first added to v5 after v5 had already run on real databases, so
49957
+ // version-5 databases exist both with and without the column. Add it only when missing.
49958
+ version: 6,
49959
+ up(db) {
49960
+ if (!hasColumn(db, "transcript_files", "head_hash"))
49961
+ db.exec("ALTER TABLE transcript_files ADD COLUMN head_hash TEXT");
49962
+ }
49912
49963
  }
49913
49964
  ];
49914
49965
  function vecTableSql(dims) {
@@ -50020,7 +50071,8 @@ var SqliteAdapter = class _SqliteAdapter {
50020
50071
  if (pending2.length) {
50021
50072
  this.transaction(() => {
50022
50073
  for (const m of pending2) {
50023
- this.db.exec(m.sql);
50074
+ if (m.sql) this.db.exec(m.sql);
50075
+ m.up?.(this.db);
50024
50076
  this.metaSet("schema_version", String(m.version));
50025
50077
  }
50026
50078
  });
@@ -50239,18 +50291,42 @@ var SqliteAdapter = class _SqliteAdapter {
50239
50291
  }
50240
50292
  vecSearch(embedding, k, filters) {
50241
50293
  if (!this.vectors) return [];
50242
- const raw = this.db.prepare(
50243
- "SELECT chunk_id id, distance FROM chunk_vec WHERE embedding MATCH ? AND k = ? ORDER BY distance"
50244
- ).all(embedding, BigInt(filters ? k * 4 : k));
50245
- if (!filters || !raw.length) return raw.map((r) => ({ chunkId: r.id, distance: r.distance }));
50246
50294
  const { where, params } = filterSql(filters);
50247
- const allowed = new Set(
50248
- this.db.prepare(
50249
- `SELECT c.id FROM chunks c JOIN documents d ON d.id = c.document_id
50250
- WHERE c.id IN (${raw.map(() => "?").join(",")})${where}`
50251
- ).all(...raw.map((r) => r.id), ...params).map((r) => r.id)
50252
- );
50253
- return raw.filter((r) => allowed.has(r.id)).slice(0, k).map((r) => ({ chunkId: r.id, distance: r.distance }));
50295
+ if (!where) return this.knn(embedding, k);
50296
+ try {
50297
+ return this.knn(
50298
+ embedding,
50299
+ k,
50300
+ `AND chunk_id IN (SELECT c.id FROM chunks c JOIN documents d ON d.id = c.document_id WHERE 1=1${where})`,
50301
+ params
50302
+ );
50303
+ } catch {
50304
+ return this.vecSearchWidening(embedding, k, where, params);
50305
+ }
50306
+ }
50307
+ knn(embedding, k, extra = "", params = []) {
50308
+ return this.db.prepare(
50309
+ `SELECT chunk_id id, distance FROM chunk_vec WHERE embedding MATCH ? AND k = ? ${extra} ORDER BY distance`
50310
+ ).all(embedding, BigInt(k), ...params).map((r) => ({ chunkId: r.id, distance: r.distance }));
50311
+ }
50312
+ /** Fallback filtered KNN: fetch k*4, k*16, ... nearest until k pass the filter or all were seen. */
50313
+ vecSearchWidening(embedding, k, where, params) {
50314
+ const total = this.vecCount();
50315
+ for (let fetch2 = k * 4; ; fetch2 *= 4) {
50316
+ const raw = this.knn(embedding, Math.min(fetch2, total));
50317
+ if (!raw.length) return [];
50318
+ const allowed = /* @__PURE__ */ new Set();
50319
+ for (let i = 0; i < raw.length; i += 400) {
50320
+ const ids = raw.slice(i, i + 400).map((r) => r.chunkId);
50321
+ for (const r of this.db.prepare(
50322
+ `SELECT c.id FROM chunks c JOIN documents d ON d.id = c.document_id
50323
+ WHERE c.id IN (${ids.map(() => "?").join(",")})${where}`
50324
+ ).all(...ids, ...params))
50325
+ allowed.add(r.id);
50326
+ }
50327
+ const hits = raw.filter((r) => allowed.has(r.chunkId)).slice(0, k);
50328
+ if (hits.length >= k || fetch2 >= total) return hits;
50329
+ }
50254
50330
  }
50255
50331
  unembeddedChunkIds(limit) {
50256
50332
  return this.db.prepare("SELECT id FROM chunks WHERE embedded = 0 ORDER BY id LIMIT ?").all(limit).map((r) => r.id);
@@ -50529,6 +50605,26 @@ var SqliteAdapter = class _SqliteAdapter {
50529
50605
  }
50530
50606
  return out;
50531
50607
  }
50608
+ graphDependentFileCounts(paths2, kinds) {
50609
+ const out = /* @__PURE__ */ new Map();
50610
+ if (!paths2.length || !kinds.length) return out;
50611
+ const kph = kinds.map(() => "?").join(",");
50612
+ for (let i = 0; i < paths2.length; i += 400) {
50613
+ const batch = paths2.slice(i, i + 400);
50614
+ const pph = batch.map(() => "?").join(",");
50615
+ for (const r of this.db.prepare(
50616
+ `SELECT d.path p, COUNT(DISTINCT s.path) n
50617
+ FROM graph_nodes d
50618
+ JOIN graph_edges e ON e.dst = d.id AND e.kind IN (${kph})
50619
+ JOIN graph_nodes s ON s.id = e.src
50620
+ WHERE d.path IN (${pph}) AND s.path IS NOT NULL AND s.path <> d.path
50621
+ GROUP BY d.path`
50622
+ ).all(...kinds, ...batch)) {
50623
+ out.set(r.p, r.n);
50624
+ }
50625
+ }
50626
+ return out;
50627
+ }
50532
50628
  edgesWhere(col, ids, kinds) {
50533
50629
  if (!ids.length) return [];
50534
50630
  const out = [];
@@ -52664,6 +52760,25 @@ function findOnPath(bin) {
52664
52760
  }
52665
52761
  return void 0;
52666
52762
  }
52763
+ var 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);';
52764
+ function requirePreamble(code) {
52765
+ if (!/\brequire\s*\(/.test(code)) return void 0;
52766
+ if (/\b(?:const|let|var|function)\s+require\b/.test(code) || /\bcreateRequire\b/.test(code)) {
52767
+ return void 0;
52768
+ }
52769
+ return `${REQUIRE_PREAMBLE}
52770
+ `;
52771
+ }
52772
+ function prepareCode(lang, code) {
52773
+ const pre = lang.preamble?.(code);
52774
+ if (!pre) return code;
52775
+ if (code.startsWith("#!")) {
52776
+ const nl = code.indexOf("\n");
52777
+ return nl === -1 ? `${code}
52778
+ ${pre}` : code.slice(0, nl + 1) + pre + code.slice(nl + 1);
52779
+ }
52780
+ return pre + code;
52781
+ }
52667
52782
  function preferredShell() {
52668
52783
  const sh = process.env.SHELL ?? "";
52669
52784
  if (/\/(zsh|bash)$/.test(sh) && existsSync11(sh)) return sh;
@@ -52696,14 +52811,16 @@ var LANGUAGES = [
52696
52811
  aliases: ["js", "node", "mjs"],
52697
52812
  ext: ".mjs",
52698
52813
  detect: () => process.execPath,
52699
- argv: (bin, file3) => [bin, "--no-warnings", file3]
52814
+ argv: (bin, file3) => [bin, "--no-warnings", file3],
52815
+ preamble: requirePreamble
52700
52816
  },
52701
52817
  {
52702
52818
  id: "typescript",
52703
52819
  aliases: ["ts"],
52704
52820
  ext: ".ts",
52705
52821
  detect: () => process.execPath,
52706
- argv: (bin, file3) => [bin, "--no-warnings", tsxCli(), file3]
52822
+ argv: (bin, file3) => [bin, "--no-warnings", tsxCli(), file3],
52823
+ preamble: requirePreamble
52707
52824
  },
52708
52825
  {
52709
52826
  id: "python",
@@ -52889,7 +53006,7 @@ async function execute(ctx, input2) {
52889
53006
  );
52890
53007
  }
52891
53008
  }
52892
- const file3 = writeScratch(input2.code, lang.ext);
53009
+ const file3 = writeScratch(prepareCode(lang, input2.code), lang.ext);
52893
53010
  const env = buildEnv(ctx.config.sandbox.envAllow, {
52894
53011
  EMINIAR_PROJECT_ROOT: ctx.root,
52895
53012
  NODE_PATH: [join17(ctx.root, "node_modules"), process.env.NODE_PATH].filter(Boolean).join(":"),
@@ -53339,10 +53456,22 @@ searchable terms: ${terms.join(", ")}`);
53339
53456
  }
53340
53457
 
53341
53458
  // src/tools/em_batch_execute.ts
53342
- var INLINE_LIMIT = 1024;
53459
+ var INLINE_LIMIT_WITH_QUERIES = 1024;
53460
+ var DEFAULT_MAX_TOKENS = 2e3;
53461
+ function chooseInline(lengths, perOutputCap, budgetChars) {
53462
+ const candidates2 = lengths.map((len, i) => ({ len, i })).filter((c) => c.len <= perOutputCap).sort((a, b) => a.len - b.len);
53463
+ const inline = /* @__PURE__ */ new Set();
53464
+ let remaining = budgetChars;
53465
+ for (const c of candidates2) {
53466
+ if (c.len > remaining) break;
53467
+ inline.add(c.i);
53468
+ remaining -= c.len;
53469
+ }
53470
+ return inline;
53471
+ }
53343
53472
  var emBatchExecute = defineTool({
53344
53473
  name: "em_batch_execute",
53345
- description: "Run several labelled commands in parallel, index every output under its label, and answer `queries` against all of them in the same round trip. Ideal for research: git status + recent commits + test output + dependency list in one call. Small outputs are returned inline; large ones only through the query hits.",
53474
+ description: "Run several labelled commands in parallel, index every output under its label, and answer `queries` against all of them in the same round trip. Ideal for research: git status + recent commits + test output + dependency list in one call. Without `queries`/`intent`, each output up to the index threshold (5 KB) is returned inline, within `max_tokens` overall; with them, only outputs up to 1 KB are inlined and the query hits carry the rest. Every output is indexed; an elided one is fetched with em_search {document_ids: [N]}.",
53346
53475
  schema: {
53347
53476
  commands: external_exports.array(
53348
53477
  external_exports.object({
@@ -53363,6 +53492,9 @@ var emBatchExecute = defineTool({
53363
53492
  const concurrency = args.concurrency ?? 4;
53364
53493
  const results = [];
53365
53494
  let rawChars = 0;
53495
+ const queries = [...args.queries ?? []];
53496
+ if (args.intent && !queries.includes(args.intent)) queries.unshift(args.intent);
53497
+ const maxTokens = args.max_tokens ?? DEFAULT_MAX_TOKENS;
53366
53498
  const queue = [...args.commands];
53367
53499
  const worker = async () => {
53368
53500
  for (; ; ) {
@@ -53384,7 +53516,7 @@ var emBatchExecute = defineTool({
53384
53516
  results.push({
53385
53517
  label: cmd.label,
53386
53518
  header: describeRun(r),
53387
- inline: r.stdout.length <= INLINE_LIMIT ? r.stdout : void 0,
53519
+ stdout: r.stdout,
53388
53520
  docId: p.documentId,
53389
53521
  stderr: r.exitCode !== 0 ? r.stderr.trim().slice(-800) : void 0
53390
53522
  });
@@ -53396,15 +53528,21 @@ var emBatchExecute = defineTool({
53396
53528
  await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, worker));
53397
53529
  const order = new Map(args.commands.map((c, i) => [c.label, i]));
53398
53530
  results.sort((a, b) => (order.get(a.label) ?? 0) - (order.get(b.label) ?? 0));
53531
+ const perOutputCap = queries.length ? INLINE_LIMIT_WITH_QUERIES : ctx.config.sandbox.indexThreshold;
53532
+ const inline = chooseInline(
53533
+ results.map((r) => r.stdout?.length ?? Number.POSITIVE_INFINITY),
53534
+ perOutputCap,
53535
+ maxTokens * 4
53536
+ );
53399
53537
  const lines = [];
53400
- for (const r of results) {
53401
- lines.push(`## ${r.label} \u2014 ${r.header}${r.docId ? ` \xB7 doc ${r.docId}` : ""}`);
53402
- if (r.inline !== void 0) lines.push(r.inline.trimEnd());
53538
+ results.forEach((r, i) => {
53539
+ const shown = inline.has(i) && r.stdout !== void 0;
53540
+ const hint = r.docId && !shown ? ` \xB7 em_search {document_ids: [${r.docId}]} for the output` : "";
53541
+ lines.push(`## ${r.label} \u2014 ${r.header}${r.docId ? ` \xB7 doc ${r.docId}` : ""}${hint}`);
53542
+ if (shown) lines.push(r.stdout.trimEnd());
53403
53543
  if (r.stderr) lines.push(`stderr: ${r.stderr}`);
53404
53544
  lines.push("");
53405
- }
53406
- const queries = [...args.queries ?? []];
53407
- if (args.intent && !queries.includes(args.intent)) queries.unshift(args.intent);
53545
+ });
53408
53546
  const docIds = results.map((r) => r.docId).filter((d) => d !== void 0);
53409
53547
  if (queries.length && docIds.length) {
53410
53548
  const responses = [];
@@ -53414,7 +53552,7 @@ var emBatchExecute = defineTool({
53414
53552
  );
53415
53553
  }
53416
53554
  lines.push("# query hits");
53417
- lines.push(formatResults(responses, args.max_tokens ?? 2e3));
53555
+ lines.push(formatResults(responses, maxTokens));
53418
53556
  }
53419
53557
  const out = lines.join("\n");
53420
53558
  recordStat(ctx, "em_batch_execute", {
@@ -54476,6 +54614,9 @@ var NAME_RE2 = /^[a-z0-9][a-z0-9._-]{0,63}$/;
54476
54614
  function docsTag(name) {
54477
54615
  return `${DOCS_PREFIX}${name}`;
54478
54616
  }
54617
+ function nameFromTag(tag) {
54618
+ return tag.startsWith(DOCS_PREFIX) ? tag.slice(DOCS_PREFIX.length) : void 0;
54619
+ }
54479
54620
  function isValidName2(name) {
54480
54621
  return NAME_RE2.test(name);
54481
54622
  }
@@ -56927,7 +57068,7 @@ function removeDocSource(ctx, db, name) {
56927
57068
  const removed = removeSource(ctx, name);
56928
57069
  const documents = db.deleteDocumentsBySource(docsTag(name));
56929
57070
  clearSyncState(db, name);
56930
- if (removed) syncDocsGraphQuietly(ctx, db);
57071
+ if (removed || documents) syncDocsGraphQuietly(ctx, db);
56931
57072
  return { removed, documents };
56932
57073
  }
56933
57074
  function formatAdd(o) {
@@ -56998,6 +57139,26 @@ function ago(ts) {
56998
57139
  if (s < 172800) return `${Math.round(s / 3600)}h ago`;
56999
57140
  return `${Math.round(s / 86400)}d ago`;
57000
57141
  }
57142
+ function orphanedDocSources(db, registered, stats = db.documentSourceStats(DOCS_PREFIX)) {
57143
+ const known = new Set(registered);
57144
+ const out = [];
57145
+ for (const s of stats) {
57146
+ const name = nameFromTag(s.source);
57147
+ if (name === void 0 || known.has(name)) continue;
57148
+ out.push({ name, documents: s.documents, chunks: s.chunks, latest: s.latest });
57149
+ }
57150
+ return out;
57151
+ }
57152
+ function formatOrphans(orphans2) {
57153
+ const names = orphans2.map((o) => o.name);
57154
+ return [
57155
+ `# Orphaned in the index (${orphans2.length}; pages without a manifest entry)`,
57156
+ ...orphans2.map(
57157
+ (o) => `- ${o.name} \xB7 ${o.documents} doc(s) \xB7 ${o.chunks} chunks \xB7 indexed ${ago(o.latest)}`
57158
+ ),
57159
+ `em_docs {action:"remove", names:${JSON.stringify(names)}} drops them; {action:"add", pick:"recommended"} or {action:"add", name, url|path} re-registers.`
57160
+ ];
57161
+ }
57001
57162
  function formatDiscovery(d, max = 40) {
57002
57163
  const lines = [];
57003
57164
  lines.push(
@@ -57145,19 +57306,30 @@ var emDocs = defineTool({
57145
57306
  case "remove": {
57146
57307
  const names = [...args.name ? [args.name] : [], ...args.names ?? []];
57147
57308
  if (!names.length) return errorResult("remove needs `name` or `names`");
57309
+ let unknown2 = 0;
57148
57310
  const lines = names.map((n) => {
57149
57311
  const r = removeDocSource(ctx, db, n);
57150
- return r.removed ? `removed ${n} (${r.documents} document(s) dropped)` : `${n}: not registered${r.documents ? ` (${r.documents} orphan document(s) dropped)` : ""}`;
57312
+ if (r.removed) return `removed ${n} (${r.documents} document(s) dropped)`;
57313
+ if (r.documents) return `removed ${n} (orphaned; ${r.documents} document(s) dropped)`;
57314
+ unknown2++;
57315
+ return `${n}: not registered and not in the index; em_docs {action:"list"} shows both`;
57151
57316
  });
57152
- return done(lines.join("\n"));
57317
+ const out = lines.join("\n");
57318
+ return unknown2 === names.length ? errorResult(out) : done(out);
57153
57319
  }
57154
57320
  case "list": {
57155
57321
  const m = readManifest(ctx);
57156
- if (!m.sources.length)
57157
- return done(
57158
- `no documentation sources registered (${manifestPath(ctx)}). em_docs {action:"discover"} finds local docs and the libraries, services and APIs this project uses.`
57159
- );
57160
- const stats = new Map(db.documentSourceStats(DOCS_PREFIX).map((s) => [s.source, s]));
57322
+ const sourceStats = db.documentSourceStats(DOCS_PREFIX);
57323
+ const orphans2 = orphanedDocSources(
57324
+ db,
57325
+ m.sources.map((s) => s.name),
57326
+ sourceStats
57327
+ );
57328
+ if (!m.sources.length) {
57329
+ const none = `no documentation sources registered (${manifestPath(ctx)}). em_docs {action:"discover"} finds local docs and the libraries, services and APIs this project uses.`;
57330
+ return done(orphans2.length ? [none, "", ...formatOrphans(orphans2)].join("\n") : none);
57331
+ }
57332
+ const stats = new Map(sourceStats.map((s) => [s.source, s]));
57161
57333
  const catalog = mergedCatalog(ctx.config.docs.catalog);
57162
57334
  const categorised = m.sources.map((s) => ({
57163
57335
  s,
@@ -57188,9 +57360,10 @@ var emDocs = defineTool({
57188
57360
  `- ${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}`
57189
57361
  );
57190
57362
  }
57363
+ if (orphans2.length) lines.push("", ...formatOrphans(orphans2));
57191
57364
  lines.push(
57192
57365
  "",
57193
- `${m.sources.length} source(s); em_docs {action:"lookup", query} searches them all, {action:"sync"} refreshes.`
57366
+ `${m.sources.length} source(s)${orphans2.length ? ` + ${orphans2.length} orphaned` : ""}; em_docs {action:"lookup", query} searches them all, {action:"sync"} refreshes.`
57194
57367
  );
57195
57368
  return done(lines.join("\n"));
57196
57369
  }
@@ -57236,10 +57409,18 @@ ${r.urls.map((u) => ` ${u}`).join("\n")}
57236
57409
  raw += r.hits.reduce((n, h) => n + h.chunk.body.length, 0);
57237
57410
  responses.push(r);
57238
57411
  }
57239
- const out = formatResults(
57240
- responses,
57241
- args.max_tokens ?? ctx.config.search.defaultMaxTokens
57412
+ let out = formatResults(responses, args.max_tokens ?? ctx.config.search.defaultMaxTokens);
57413
+ const hitSources = new Set(
57414
+ responses.flatMap((r) => r.hits.map((h) => h.chunk.document.source))
57242
57415
  );
57416
+ const orphaned = orphanedDocSources(
57417
+ db,
57418
+ m.sources.map((s) => s.name)
57419
+ ).filter((o) => hitSources.has(docsTag(o.name)));
57420
+ if (orphaned.length)
57421
+ out += `
57422
+
57423
+ note: ${orphaned.map((o) => o.name).join(", ")} ${orphaned.length === 1 ? "is" : "are"} not in the manifest (orphaned pages); em_docs {action:"list"} explains`;
57243
57424
  recordStat(ctx, "em_docs", {
57244
57425
  rawChars: raw,
57245
57426
  returnedChars: out.length,
@@ -57470,7 +57651,7 @@ var LANG_ENUM = external_exports.string().describe(
57470
57651
  );
57471
57652
  var emExecute = defineTool({
57472
57653
  name: "em_execute",
57473
- description: "Run code in a sandbox inside the project root and return only stdout. Output over the index threshold (5 KB) is indexed; pass `intent` to get back just the matching sections plus a vocabulary of searchable terms. Use this instead of Bash for anything that prints a lot: git log/diff, test runs, dependency trees, logs, API calls. Shell snippets honour Claude Code's Bash deny rules.",
57654
+ description: "Run code in a sandbox inside the project root and return only stdout. Output over the index threshold (5 KB) is indexed; pass `intent` to get back just the matching sections plus a vocabulary of searchable terms. Use this instead of Bash for anything that prints a lot: git log/diff, test runs, dependency trees, logs, API calls. Shell snippets honour Claude Code's Bash deny rules. javascript and typescript run as ES modules; `import` and `require` both work.",
57474
57655
  schema: {
57475
57656
  language: LANG_ENUM,
57476
57657
  code: external_exports.string().min(1).describe("Program or shell script to run"),
@@ -57552,7 +57733,7 @@ function refuse(check3, target) {
57552
57733
  // src/tools/em_execute_file.ts
57553
57734
  var emExecuteFile = defineTool({
57554
57735
  name: "em_execute_file",
57555
- description: "Process a file in the sandbox without reading it into context. Your code receives the absolute path as $EM_FILE (shell) / process.env.EM_FILE (node) / os.environ['EM_FILE'] (python); print only what you need. The file must be inside the project root unless a Read(<path>) allow rule opts it in; a matching Read deny or ask rule refuses the path, inside the root or out. Omit `code` to get a quick summary (size, lines, head).",
57736
+ description: "Process a file in the sandbox without reading it into context. Your code receives the absolute path as $EM_FILE (shell) / process.env.EM_FILE (node) / os.environ['EM_FILE'] (python); print only what you need. The file must be inside the project root unless a Read(<path>) allow rule opts it in; a matching Read deny or ask rule refuses the path, inside the root or out. Omit `code` to get a quick summary (size, lines, head). javascript and typescript run as ES modules; `import` and `require` both work.",
57556
57737
  schema: {
57557
57738
  path: external_exports.string().describe("File to process, relative to the project root or absolute"),
57558
57739
  language: LANG_ENUM.optional(),
@@ -58528,7 +58709,7 @@ var emIndex = defineTool({
58528
58709
  });
58529
58710
  kickEmbedding(ctx, db);
58530
58711
  return text(
58531
- `${r.created ? "indexed" : "unchanged"} "${args.title}" as doc ${r.documentId}: ${r.chunks} chunks, ${r.chars} chars (${r.kind})`
58712
+ `${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)" : ""}`
58532
58713
  );
58533
58714
  }
58534
58715
  if (!args.path) return errorResult("provide `path` or `content`");
@@ -58538,7 +58719,7 @@ var emIndex = defineTool({
58538
58719
  recursive: args.recursive ?? true
58539
58720
  });
58540
58721
  const lines = [
58541
- `${args.path}: ${s.files} file(s), ${s.indexed} indexed, ${s.unchanged} unchanged, ${s.chunks} chunks`
58722
+ `${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)` : ""}`
58542
58723
  ];
58543
58724
  for (const sk of s.skipped.slice(0, 10)) lines.push(` skipped ${sk.path}: ${sk.reason}`);
58544
58725
  if (s.skipped.length > 10) lines.push(` \u2026 ${s.skipped.length - 10} more skipped`);
@@ -58914,7 +59095,10 @@ var emSearch = defineTool({
58914
59095
  }
58915
59096
  }
58916
59097
  const out = formatResults(responses, args.max_tokens ?? ctx.config.search.defaultMaxTokens);
58917
- const note = mode !== "lexical" && mode !== "regex" && !vectorSearch && ctx.config.embeddings.enabled ? "\n\n(embeddings: model still loading \u2014 lexical results only this time)" : "";
59098
+ let note = mode !== "lexical" && mode !== "regex" && !vectorSearch && ctx.config.embeddings.enabled ? "\n\n(embeddings: model still loading \u2014 lexical results only this time)" : "";
59099
+ const sources = Array.isArray(args.source) ? args.source : args.source ? [args.source] : [];
59100
+ if (mode === "semantic" && !ctx.config.docs.embed && sources.some((s) => s.startsWith("docs:")) && responses.every((r) => r.hits.length === 0))
59101
+ 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})';
58918
59102
  recordStat(ctx, "em_search", {
58919
59103
  rawChars: responses.reduce(
58920
59104
  (a, r) => a + r.hits.reduce((b, h) => b + h.chunk.body.length, 0),
@@ -58933,6 +59117,7 @@ init_esm_shims();
58933
59117
  // src/session/recall.ts
58934
59118
  init_esm_shims();
58935
59119
  init_ids();
59120
+ init_metrics();
58936
59121
  init_model();
58937
59122
  var RECALL_CATEGORY = "recall";
58938
59123
  function recallStats(events) {
@@ -60875,12 +61060,17 @@ function usageLine(ctx, sessionId) {
60875
61060
  if (!usageEnabled(ctx)) return void 0;
60876
61061
  try {
60877
61062
  const db = getStorage(ctx);
60878
- ingestSession(db, ctx.root, sessionId, void 0, { overrides: ctx.config.usage.pricing });
61063
+ let failed;
61064
+ try {
61065
+ ingestSession(db, ctx.root, sessionId, void 0, { overrides: ctx.config.usage.pricing });
61066
+ } catch (err) {
61067
+ failed = `usage: ingest failed: ${err.message}`;
61068
+ }
60879
61069
  const b = usageBrief(db, ctx, { sessionId });
60880
- if (!b.todayUsd && !b.sessionUsd) return void 0;
60881
- return `usage: today ${formatUsd(b.todayUsd)} \xB7 session ${formatUsd(b.sessionUsd)} \xB7 see em_usage`;
60882
- } catch {
60883
- return void 0;
61070
+ const line = b.todayUsd || b.sessionUsd ? `usage: today ${formatUsd(b.todayUsd)} \xB7 session ${formatUsd(b.sessionUsd)} \xB7 see em_usage` : void 0;
61071
+ return [line, failed].filter((s) => s).join("\n") || void 0;
61072
+ } catch (err) {
61073
+ return `usage: unavailable: ${err.message}`;
60884
61074
  }
60885
61075
  }
60886
61076
  function mergeByTool(rows) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eminiar",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Local-first context, memory and knowledge-graph plugin for Claude Code. Sandboxed execution, FTS5 + vector search, session continuity, and a code graph — nothing leaves your machine.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,6 +20,8 @@ Act on `$ARGUMENTS` with `em_docs`; with no arguments, run `list` and, when it i
20
20
  one source with `name`. Cite the `doc N` ids. Only when the answer is not in the index should
21
21
  you fall back to `em_fetch_and_index` for a specific page, then `{action: "add", url}` if it is
22
22
  worth keeping.
23
- - **list** / **remove** → the matching action.
23
+ - **list** / **remove** → the matching action. `list` also shows sources *orphaned in the
24
+ index* (pages indexed under `docs:<name>` with no manifest entry, e.g. the manifest lives on
25
+ another branch); `remove` drops their pages, `add` with the same name re-registers them.
24
26
 
25
27
  The manifest `.eminiar/docs.json` is meant to be committed; say so once when sources were added.