@dotdotgod/hermes 0.6.4 → 0.6.5

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.
Files changed (3) hide show
  1. package/mcp/cli.mjs +46 -11
  2. package/package.json +2 -2
  3. package/plugin.yaml +1 -1
package/mcp/cli.mjs CHANGED
@@ -855,6 +855,15 @@ import { homedir } from "node:os";
855
855
  import { join } from "node:path";
856
856
  var DEFAULT_EMBEDDING_PROFILE = Object.freeze({ provider: "local", model: "Xenova/multilingual-e5-small" });
857
857
  var EMBEDDING_PROVIDERS = Object.freeze(["local", "openai-compatible", "ollama"]);
858
+ var DEFAULT_INPUT_FORMAT = Object.freeze({ queryPrefix: "query: ", documentPrefix: "passage: " });
859
+ function embeddingInputFormat(profile, label = "embedding") {
860
+ if (profile.inputFormat === void 0) return { ...DEFAULT_INPUT_FORMAT };
861
+ const format = profile.inputFormat;
862
+ if (!format || typeof format !== "object" || Array.isArray(format) || Object.keys(format).some((key) => !Object.hasOwn(DEFAULT_INPUT_FORMAT, key)) || Object.keys(DEFAULT_INPUT_FORMAT).some((key) => typeof format[key] !== "string")) {
863
+ throw new Error(`${label}.inputFormat must contain exactly queryPrefix and documentPrefix strings.`);
864
+ }
865
+ return { queryPrefix: format.queryPrefix, documentPrefix: format.documentPrefix };
866
+ }
858
867
  function readJson(path, label) {
859
868
  if (!existsSync(path)) return null;
860
869
  try {
@@ -865,7 +874,7 @@ function readJson(path, label) {
865
874
  }
866
875
  function validateEmbeddingProfile(profile, label = "embedding") {
867
876
  if (!profile || typeof profile !== "object" || Array.isArray(profile)) throw new Error(`${label} must be an object.`);
868
- const allowed = /* @__PURE__ */ new Set(["provider", "model", "baseUrl", "apiKey", "apiKeyEnv"]);
877
+ const allowed = /* @__PURE__ */ new Set(["provider", "model", "baseUrl", "apiKey", "apiKeyEnv", "inputFormat"]);
869
878
  const unknown = Object.keys(profile).find((key) => !allowed.has(key));
870
879
  if (unknown) throw new Error(`${label}.${unknown} is not supported.`);
871
880
  if (!EMBEDDING_PROVIDERS.includes(profile.provider)) throw new Error(`${label}.provider must be local, openai-compatible, or ollama.`);
@@ -883,6 +892,7 @@ function validateEmbeddingProfile(profile, label = "embedding") {
883
892
  if (!["http:", "https:"].includes(url.protocol)) throw new Error(`${label}.baseUrl must use HTTP or HTTPS.`);
884
893
  if (url.username || url.password) throw new Error(`${label}.baseUrl must not contain credentials.`);
885
894
  }
895
+ embeddingInputFormat(profile, label);
886
896
  return { ...profile, model: profile.model.trim(), ...profile.baseUrl ? { baseUrl: profile.baseUrl.replace(/\/$/, "") } : {} };
887
897
  }
888
898
  function sanitizeEmbeddingProfile(profile) {
@@ -890,8 +900,13 @@ function sanitizeEmbeddingProfile(profile) {
890
900
  return { ...safe, ...apiKey ? { apiKey: "[redacted]" } : {} };
891
901
  }
892
902
  function embeddingProfileIdentity(profile) {
893
- const { apiKey, apiKeyEnv, ...identity2 } = profile;
894
- return identity2;
903
+ return {
904
+ provider: profile.provider,
905
+ model: profile.model,
906
+ ...profile.baseUrl !== void 0 ? { baseUrl: profile.baseUrl } : {},
907
+ inputFormatVersion: 1,
908
+ inputFormat: embeddingInputFormat(profile)
909
+ };
895
910
  }
896
911
  function resolveEmbeddingProfile(root = ".", options = {}) {
897
912
  const home = options.home ?? homedir();
@@ -3944,7 +3959,12 @@ async function postJson(url, body, profile) {
3944
3959
  const response = await fetch(url, { method: "POST", headers: { "content-type": "application/json", ...key ? { authorization: `Bearer ${key}` } : {} }, body: JSON.stringify(body), signal: controller.signal });
3945
3960
  if (!response.ok) {
3946
3961
  const category = response.status === 401 || response.status === 403 ? "authentication" : response.status === 429 ? "rate limit" : `HTTP ${response.status}`;
3947
- throw new Error(`Remote embedding ${category} failure.`);
3962
+ const error = new Error(`Remote embedding ${category} failure.`);
3963
+ if (response.status === 400 || response.status === 413) {
3964
+ const body2 = (await response.text()).slice(0, 8192);
3965
+ error.tokenLimit = /(?:exceed\w*[\s\S]{0,120}tokens?|tokens?[\s\S]{0,120}(?:exceed|limit|too (?:long|many))|maximum context length)/i.test(body2);
3966
+ }
3967
+ throw error;
3948
3968
  }
3949
3969
  try {
3950
3970
  return await response.json();
@@ -3971,20 +3991,35 @@ function remoteEmbedder(profile) {
3971
3991
  return ordered.map(normalize);
3972
3992
  };
3973
3993
  }
3994
+ function withInputFormat(embed, profile) {
3995
+ const format = embeddingInputFormat(profile);
3996
+ return (texts, role) => {
3997
+ if (role !== void 0 && role !== "query" && role !== "document") throw new Error("Embedding role must be query or document.");
3998
+ return embed(role === void 0 ? texts : texts.map((text) => format[`${role}Prefix`] + text));
3999
+ };
4000
+ }
3974
4001
  async function createEmbedder(profile) {
3975
4002
  const embed = profile.provider === "local" ? await localEmbedder(profile) : remoteEmbedder(profile);
3976
- return async (texts) => {
4003
+ async function embedBatch(texts) {
3977
4004
  if (texts.length === 0) return [];
3978
- const rows = await embed(texts);
4005
+ let rows;
4006
+ try {
4007
+ rows = await embed(texts);
4008
+ } catch (error) {
4009
+ if (!error.tokenLimit || texts.length === 1) throw error;
4010
+ const middle = Math.ceil(texts.length / 2);
4011
+ rows = [...await embedBatch(texts.slice(0, middle)), ...await embedBatch(texts.slice(middle))];
4012
+ }
3979
4013
  if (!Array.isArray(rows) || rows.length !== texts.length) throw new Error("Embedding provider returned an unexpected vector count.");
3980
4014
  const dimensions = rows[0]?.length;
3981
4015
  if (!dimensions || rows.some((row) => row.length !== dimensions)) throw new Error("Embedding provider returned inconsistent vector dimensions.");
3982
4016
  return rows;
3983
- };
4017
+ }
4018
+ return withInputFormat(embedBatch, profile);
3984
4019
  }
3985
4020
  async function resolveEmbedder(root, options = {}) {
3986
4021
  const resolved = options.profile ? { profile: options.profile, source: options.source ?? "injected", path: null } : resolveEmbeddingProfile(root, options);
3987
- return { ...resolved, identity: embeddingProfileIdentity(resolved.profile), embed: options.embed ?? await createEmbedder(resolved.profile) };
4022
+ return { ...resolved, identity: embeddingProfileIdentity(resolved.profile), embed: options.embed ? withInputFormat(options.embed, resolved.profile) : await createEmbedder(resolved.profile) };
3988
4023
  }
3989
4024
 
3990
4025
  // packages/cli/src/query/store.mjs
@@ -4190,7 +4225,7 @@ async function buildVectorIndex(root, embedOrOptions = {}) {
4190
4225
  });
4191
4226
  for (let offset = 0; offset < missing.length; offset += EMBED_BATCH_SIZE) {
4192
4227
  const batch = missing.slice(offset, offset + EMBED_BATCH_SIZE);
4193
- const embedded = await resolved.embed(batch.map(({ chunk }) => `passage: ${chunk.passage}`));
4228
+ const embedded = await resolved.embed(batch.map(({ chunk }) => chunk.passage), "document");
4194
4229
  embedded.forEach((vector, index) => {
4195
4230
  rows[batch[index].index] = vector;
4196
4231
  });
@@ -4220,7 +4255,7 @@ async function queryProject(root, query, options = {}) {
4220
4255
  try {
4221
4256
  assertCodeEmbeddingConsent(root, options, corpus.chunks);
4222
4257
  index = await buildVectorIndex(root, { ...options, chunks: corpus.chunks });
4223
- const [queryVector] = await index.embedding.embed([`query: ${query}`]);
4258
+ const [queryVector] = await index.embedding.embed([query], "query");
4224
4259
  vector = index.chunks.map((chunk, position) => ({ ...chunk, vectorScore: cosineScore(queryVector, index.vectors, position * index.manifest.dimensions, index.manifest.dimensions) })).sort((a, b) => b.vectorScore - a.vectorScore || a.id.localeCompare(b.id));
4225
4260
  } catch (error) {
4226
4261
  if (search === "vector") throw error;
@@ -5996,7 +6031,7 @@ async function buildVectorImpactOverlay(root, index, changedPaths, options = {})
5996
6031
  if (!profile) continue;
5997
6032
  const embed = vectorIndex.embedding?.embed ?? options.embed;
5998
6033
  if (!embed) throw new Error("Vector index did not provide an embedder.");
5999
- const [queryVector] = await embed([`query: ${profile.text}`]);
6034
+ const [queryVector] = await embed([profile.text], "query");
6000
6035
  if (!queryVector) throw new Error("Changed-file embedding was empty.");
6001
6036
  const matches = rankVectorFiles(queryVector, vectorIndex).filter((match) => match.path !== changed && match.vectorScore >= threshold).slice(0, topK);
6002
6037
  for (const match of matches) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdotgod/hermes",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Hermes CLI and messaging-gateway adapter without Plan Mode.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -13,7 +13,7 @@
13
13
  "devDependencies": {
14
14
  "@modelcontextprotocol/sdk": "^1.25.2",
15
15
  "esbuild": "0.27.3",
16
- "@dotdotgod/context": "0.6.4"
16
+ "@dotdotgod/context": "0.6.5"
17
17
  },
18
18
  "files": [
19
19
  "plugin.yaml",
package/plugin.yaml CHANGED
@@ -1,5 +1,5 @@
1
1
  name: dotdotgod
2
- version: 0.6.4
2
+ version: 0.6.5
3
3
  description: Project memory, isolated context tools and impact checks for Hermes CLI and messaging gateway; no Plan Mode.
4
4
  author: dotdotgod
5
5
  license: Elastic-2.0