nemoris 0.1.0

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 (223) hide show
  1. package/.env.example +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +209 -0
  4. package/SECURITY.md +119 -0
  5. package/bin/nemoris +46 -0
  6. package/config/agents/agent.toml.example +28 -0
  7. package/config/agents/default.toml +22 -0
  8. package/config/agents/orchestrator.toml +18 -0
  9. package/config/delivery.toml +73 -0
  10. package/config/embeddings.toml +5 -0
  11. package/config/identity/default-purpose.md +1 -0
  12. package/config/identity/default-soul.md +3 -0
  13. package/config/identity/orchestrator-purpose.md +1 -0
  14. package/config/identity/orchestrator-soul.md +1 -0
  15. package/config/improvement-targets.toml +15 -0
  16. package/config/jobs/heartbeat-check.toml +30 -0
  17. package/config/jobs/memory-rollup.toml +46 -0
  18. package/config/jobs/workspace-health.toml +63 -0
  19. package/config/mcp.toml +16 -0
  20. package/config/output-contracts.toml +17 -0
  21. package/config/peers.toml +32 -0
  22. package/config/peers.toml.example +32 -0
  23. package/config/policies/memory-default.toml +10 -0
  24. package/config/policies/memory-heartbeat.toml +5 -0
  25. package/config/policies/memory-ops.toml +10 -0
  26. package/config/policies/tools-heartbeat-minimal.toml +8 -0
  27. package/config/policies/tools-interactive-safe.toml +8 -0
  28. package/config/policies/tools-ops-bounded.toml +8 -0
  29. package/config/policies/tools-orchestrator.toml +7 -0
  30. package/config/providers/anthropic.toml +15 -0
  31. package/config/providers/ollama.toml +5 -0
  32. package/config/providers/openai-codex.toml +9 -0
  33. package/config/providers/openrouter.toml +5 -0
  34. package/config/router.toml +22 -0
  35. package/config/runtime.toml +114 -0
  36. package/config/skills/self-improvement.toml +15 -0
  37. package/config/skills/telegram-onboarding-spec.md +240 -0
  38. package/config/skills/workspace-monitor.toml +15 -0
  39. package/config/task-router.toml +42 -0
  40. package/install.sh +50 -0
  41. package/package.json +90 -0
  42. package/src/auth/auth-profiles.js +169 -0
  43. package/src/auth/openai-codex-oauth.js +285 -0
  44. package/src/battle.js +449 -0
  45. package/src/cli/help.js +265 -0
  46. package/src/cli/output-filter.js +49 -0
  47. package/src/cli/runtime-control.js +704 -0
  48. package/src/cli-main.js +2763 -0
  49. package/src/cli.js +78 -0
  50. package/src/config/loader.js +332 -0
  51. package/src/config/schema-validator.js +214 -0
  52. package/src/config/toml-lite.js +8 -0
  53. package/src/daemon/action-handlers.js +71 -0
  54. package/src/daemon/healing-tick.js +87 -0
  55. package/src/daemon/health-probes.js +90 -0
  56. package/src/daemon/notifier.js +57 -0
  57. package/src/daemon/nurse.js +218 -0
  58. package/src/daemon/repair-log.js +106 -0
  59. package/src/daemon/rule-staging.js +90 -0
  60. package/src/daemon/rules.js +29 -0
  61. package/src/daemon/telegram-commands.js +54 -0
  62. package/src/daemon/updater.js +85 -0
  63. package/src/jobs/job-runner.js +78 -0
  64. package/src/mcp/consumer.js +129 -0
  65. package/src/memory/active-recall.js +171 -0
  66. package/src/memory/backend-manager.js +97 -0
  67. package/src/memory/backends/file-backend.js +38 -0
  68. package/src/memory/backends/qmd-backend.js +219 -0
  69. package/src/memory/embedding-guards.js +24 -0
  70. package/src/memory/embedding-index.js +118 -0
  71. package/src/memory/embedding-service.js +179 -0
  72. package/src/memory/file-index.js +177 -0
  73. package/src/memory/memory-signature.js +5 -0
  74. package/src/memory/memory-store.js +648 -0
  75. package/src/memory/retrieval-planner.js +66 -0
  76. package/src/memory/scoring.js +145 -0
  77. package/src/memory/simhash.js +78 -0
  78. package/src/memory/sqlite-active-store.js +824 -0
  79. package/src/memory/write-policy.js +36 -0
  80. package/src/onboarding/aliases.js +33 -0
  81. package/src/onboarding/auth/api-key.js +224 -0
  82. package/src/onboarding/auth/ollama-detect.js +42 -0
  83. package/src/onboarding/clack-prompter.js +77 -0
  84. package/src/onboarding/doctor.js +530 -0
  85. package/src/onboarding/lock.js +42 -0
  86. package/src/onboarding/model-catalog.js +344 -0
  87. package/src/onboarding/phases/auth.js +589 -0
  88. package/src/onboarding/phases/build.js +130 -0
  89. package/src/onboarding/phases/choose.js +82 -0
  90. package/src/onboarding/phases/detect.js +98 -0
  91. package/src/onboarding/phases/hatch.js +216 -0
  92. package/src/onboarding/phases/identity.js +79 -0
  93. package/src/onboarding/phases/ollama.js +345 -0
  94. package/src/onboarding/phases/scaffold.js +99 -0
  95. package/src/onboarding/phases/telegram.js +377 -0
  96. package/src/onboarding/phases/validate.js +204 -0
  97. package/src/onboarding/phases/verify.js +206 -0
  98. package/src/onboarding/platform.js +482 -0
  99. package/src/onboarding/status-bar.js +95 -0
  100. package/src/onboarding/templates.js +794 -0
  101. package/src/onboarding/toml-writer.js +38 -0
  102. package/src/onboarding/tui.js +250 -0
  103. package/src/onboarding/uninstall.js +153 -0
  104. package/src/onboarding/wizard.js +499 -0
  105. package/src/providers/anthropic.js +168 -0
  106. package/src/providers/base.js +247 -0
  107. package/src/providers/circuit-breaker.js +136 -0
  108. package/src/providers/ollama.js +163 -0
  109. package/src/providers/openai-codex.js +149 -0
  110. package/src/providers/openrouter.js +136 -0
  111. package/src/providers/registry.js +36 -0
  112. package/src/providers/router.js +16 -0
  113. package/src/runtime/bootstrap-cache.js +47 -0
  114. package/src/runtime/capabilities-prompt.js +25 -0
  115. package/src/runtime/completion-ping.js +99 -0
  116. package/src/runtime/config-validator.js +121 -0
  117. package/src/runtime/context-ledger.js +360 -0
  118. package/src/runtime/cutover-readiness.js +42 -0
  119. package/src/runtime/daemon.js +729 -0
  120. package/src/runtime/delivery-ack.js +195 -0
  121. package/src/runtime/delivery-adapters/local-file.js +41 -0
  122. package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
  123. package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
  124. package/src/runtime/delivery-adapters/shadow.js +13 -0
  125. package/src/runtime/delivery-adapters/standalone-http.js +98 -0
  126. package/src/runtime/delivery-adapters/telegram.js +104 -0
  127. package/src/runtime/delivery-adapters/tui.js +128 -0
  128. package/src/runtime/delivery-manager.js +807 -0
  129. package/src/runtime/delivery-store.js +168 -0
  130. package/src/runtime/dependency-health.js +118 -0
  131. package/src/runtime/envelope.js +114 -0
  132. package/src/runtime/evaluation.js +1089 -0
  133. package/src/runtime/exec-approvals.js +216 -0
  134. package/src/runtime/executor.js +500 -0
  135. package/src/runtime/failure-ping.js +67 -0
  136. package/src/runtime/flows.js +83 -0
  137. package/src/runtime/guards.js +45 -0
  138. package/src/runtime/handoff.js +51 -0
  139. package/src/runtime/identity-cache.js +28 -0
  140. package/src/runtime/improvement-engine.js +109 -0
  141. package/src/runtime/improvement-harness.js +581 -0
  142. package/src/runtime/input-sanitiser.js +72 -0
  143. package/src/runtime/interaction-contract.js +347 -0
  144. package/src/runtime/lane-readiness.js +226 -0
  145. package/src/runtime/migration.js +323 -0
  146. package/src/runtime/model-resolution.js +78 -0
  147. package/src/runtime/network.js +64 -0
  148. package/src/runtime/notification-store.js +97 -0
  149. package/src/runtime/notifier.js +256 -0
  150. package/src/runtime/orchestrator.js +53 -0
  151. package/src/runtime/orphan-reaper.js +41 -0
  152. package/src/runtime/output-contract-schema.js +139 -0
  153. package/src/runtime/output-contract-validator.js +439 -0
  154. package/src/runtime/peer-readiness.js +69 -0
  155. package/src/runtime/peer-registry.js +133 -0
  156. package/src/runtime/pilot-status.js +108 -0
  157. package/src/runtime/prompt-builder.js +261 -0
  158. package/src/runtime/provider-attempt.js +582 -0
  159. package/src/runtime/report-fallback.js +71 -0
  160. package/src/runtime/result-normalizer.js +183 -0
  161. package/src/runtime/retention.js +74 -0
  162. package/src/runtime/review.js +244 -0
  163. package/src/runtime/route-job.js +15 -0
  164. package/src/runtime/run-store.js +38 -0
  165. package/src/runtime/schedule.js +88 -0
  166. package/src/runtime/scheduler-state.js +434 -0
  167. package/src/runtime/scheduler.js +656 -0
  168. package/src/runtime/session-compactor.js +182 -0
  169. package/src/runtime/session-search.js +155 -0
  170. package/src/runtime/slack-inbound.js +249 -0
  171. package/src/runtime/ssrf.js +102 -0
  172. package/src/runtime/status-aggregator.js +330 -0
  173. package/src/runtime/task-contract.js +140 -0
  174. package/src/runtime/task-packet.js +107 -0
  175. package/src/runtime/task-router.js +140 -0
  176. package/src/runtime/telegram-inbound.js +1565 -0
  177. package/src/runtime/token-counter.js +134 -0
  178. package/src/runtime/token-estimator.js +59 -0
  179. package/src/runtime/tool-loop.js +200 -0
  180. package/src/runtime/transport-server.js +311 -0
  181. package/src/runtime/tui-server.js +411 -0
  182. package/src/runtime/ulid.js +44 -0
  183. package/src/security/ssrf-check.js +197 -0
  184. package/src/setup.js +369 -0
  185. package/src/shadow/bridge.js +303 -0
  186. package/src/skills/loader.js +84 -0
  187. package/src/tools/catalog.json +49 -0
  188. package/src/tools/cli-delegate.js +44 -0
  189. package/src/tools/mcp-client.js +106 -0
  190. package/src/tools/micro/cancel-task.js +6 -0
  191. package/src/tools/micro/complete-task.js +6 -0
  192. package/src/tools/micro/fail-task.js +6 -0
  193. package/src/tools/micro/http-fetch.js +74 -0
  194. package/src/tools/micro/index.js +36 -0
  195. package/src/tools/micro/lcm-recall.js +60 -0
  196. package/src/tools/micro/list-dir.js +17 -0
  197. package/src/tools/micro/list-skills.js +46 -0
  198. package/src/tools/micro/load-skill.js +38 -0
  199. package/src/tools/micro/memory-search.js +45 -0
  200. package/src/tools/micro/read-file.js +11 -0
  201. package/src/tools/micro/session-search.js +54 -0
  202. package/src/tools/micro/shell-exec.js +43 -0
  203. package/src/tools/micro/trigger-job.js +79 -0
  204. package/src/tools/micro/web-search.js +58 -0
  205. package/src/tools/micro/workspace-paths.js +39 -0
  206. package/src/tools/micro/write-file.js +14 -0
  207. package/src/tools/micro/write-memory.js +41 -0
  208. package/src/tools/registry.js +348 -0
  209. package/src/tools/tool-result-contract.js +36 -0
  210. package/src/tui/chat.js +835 -0
  211. package/src/tui/renderer.js +175 -0
  212. package/src/tui/socket-client.js +217 -0
  213. package/src/utils/canonical-json.js +29 -0
  214. package/src/utils/compaction.js +30 -0
  215. package/src/utils/env-loader.js +5 -0
  216. package/src/utils/errors.js +80 -0
  217. package/src/utils/fs.js +101 -0
  218. package/src/utils/ids.js +5 -0
  219. package/src/utils/model-context-limits.js +30 -0
  220. package/src/utils/token-budget.js +74 -0
  221. package/src/utils/usage-cost.js +25 -0
  222. package/src/utils/usage-metrics.js +14 -0
  223. package/vendor/smol-toml-1.5.2.tgz +0 -0
@@ -0,0 +1,219 @@
1
+ import path from "node:path";
2
+ import { listFilesRecursive, readText } from "../../utils/fs.js";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ function parseQmdCollections(yaml) {
9
+ const lines = String(yaml || "").split("\n");
10
+ const collections = [];
11
+ let current = null;
12
+
13
+ for (const rawLine of lines) {
14
+ const line = rawLine.trimEnd();
15
+ const trimmed = line.trim();
16
+ if (!trimmed) continue;
17
+ const collectionMatch = /^([a-zA-Z0-9_-]+):\s*$/.exec(trimmed);
18
+ if (collectionMatch && !trimmed.startsWith("collections:")) {
19
+ current = {
20
+ id: collectionMatch[1]
21
+ };
22
+ collections.push(current);
23
+ continue;
24
+ }
25
+ if (!current) continue;
26
+ const pathMatch = /^path:\s*(.+)$/.exec(trimmed);
27
+ if (pathMatch) {
28
+ current.path = pathMatch[1];
29
+ continue;
30
+ }
31
+ const patternMatch = /^pattern:\s*(.+)$/.exec(trimmed);
32
+ if (patternMatch) {
33
+ current.pattern = patternMatch[1];
34
+ }
35
+ }
36
+
37
+ return collections;
38
+ }
39
+
40
+ export class QmdMemoryBackend {
41
+ constructor({ liveRoot }) {
42
+ this.kind = "qmd";
43
+ this.liveRoot = liveRoot;
44
+ }
45
+
46
+ async inspect(agentId, workspaceRoot = null) {
47
+ if (!this.liveRoot) {
48
+ return {
49
+ kind: this.kind,
50
+ available: false,
51
+ agentId: null,
52
+ collections: [],
53
+ indexSqlite: null
54
+ };
55
+ }
56
+ const direct = await this.inspectDirect(agentId);
57
+ if (direct.available) return direct;
58
+
59
+ if (workspaceRoot) {
60
+ const byWorkspace = await this.inspectByWorkspace(workspaceRoot);
61
+ if (byWorkspace.available) {
62
+ return {
63
+ ...byWorkspace,
64
+ mappedFromWorkspace: true
65
+ };
66
+ }
67
+ }
68
+
69
+ return direct;
70
+ }
71
+
72
+ async inspectDirect(agentId) {
73
+ const qmdRoot = path.join(this.liveRoot, "agents", agentId, "qmd");
74
+ return this.inspectRoot(qmdRoot, agentId);
75
+ }
76
+
77
+ async inspectByWorkspace(workspaceRoot) {
78
+ const qmdConfigs = (await listFilesRecursive(path.join(this.liveRoot, "agents"))).filter((filePath) =>
79
+ filePath.endsWith("/qmd/xdg-config/index.yml")
80
+ );
81
+
82
+ for (const configPath of qmdConfigs) {
83
+ const configText = await readText(configPath, "");
84
+ const collections = parseQmdCollections(configText);
85
+ if (collections.some((collection) => matchesWorkspaceCollection(workspaceRoot, collection.path))) {
86
+ const agentId = configPath.split(path.sep).slice(-4, -3)[0];
87
+ const qmdRoot = path.join(this.liveRoot, "agents", agentId, "qmd");
88
+ return this.inspectRoot(qmdRoot, agentId);
89
+ }
90
+ }
91
+
92
+ return {
93
+ kind: this.kind,
94
+ available: false,
95
+ agentId: null,
96
+ collections: [],
97
+ indexSqlite: null
98
+ };
99
+ }
100
+
101
+ async inspectRoot(qmdRoot, agentId) {
102
+ const configPath = path.join(qmdRoot, "xdg-config", "index.yml");
103
+ const indexSqlite = path.join(qmdRoot, "xdg-cache", "qmd", "index.sqlite");
104
+ const configText = await readText(configPath, null);
105
+ const indexExists = await readText(indexSqlite, null).then(() => true, () => false);
106
+ const stats = indexExists ? await this.readStats(indexSqlite) : null;
107
+
108
+ return {
109
+ kind: this.kind,
110
+ available: Boolean(configText),
111
+ agentId,
112
+ configPath,
113
+ indexSqlite: indexExists ? indexSqlite : null,
114
+ collections: parseQmdCollections(configText || ""),
115
+ stats
116
+ };
117
+ }
118
+
119
+ async query(agentId, query, options = {}) {
120
+ const inspected = await this.inspect(agentId, options.workspaceRoot || null);
121
+ if (!inspected.available || !inspected.indexSqlite) {
122
+ return {
123
+ backend: this.kind,
124
+ available: false,
125
+ items: []
126
+ };
127
+ }
128
+
129
+ const normalized = normalizeFtsQuery(query);
130
+ const limit = options.limit ?? 5;
131
+ const sql =
132
+ "select documents_fts.filepath, documents_fts.title, documents.path as document_path, documents.collection, " +
133
+ "snippet(documents_fts, 2, '[', ']', ' … ', 12) as snippet " +
134
+ "from documents_fts join documents on documents.id = documents_fts.rowid " +
135
+ `where documents_fts match '${escapeSql(normalized)}' limit ${Number(limit) * 3 || 15}`;
136
+
137
+ try {
138
+ const { stdout } = await execFileAsync("sqlite3", ["-json", inspected.indexSqlite, sql]);
139
+ return {
140
+ backend: this.kind,
141
+ available: true,
142
+ items: dedupeQmdItems(JSON.parse(stdout || "[]")).slice(0, limit),
143
+ stats: inspected.stats || null
144
+ };
145
+ } catch {
146
+ return {
147
+ backend: this.kind,
148
+ available: true,
149
+ items: [],
150
+ stats: inspected.stats || null
151
+ };
152
+ }
153
+ }
154
+
155
+ async readStats(indexSqlite) {
156
+ try {
157
+ const sql =
158
+ "select " +
159
+ "(select count(*) from documents where active=1) as docs, " +
160
+ "(select count(*) from content_vectors) as vectors;";
161
+ const { stdout } = await execFileAsync("sqlite3", ["-json", indexSqlite, sql]);
162
+ return JSON.parse(stdout || "[]")[0] || null;
163
+ } catch {
164
+ return null;
165
+ }
166
+ }
167
+ }
168
+
169
+ function normalizeFtsQuery(query) {
170
+ return String(query || "")
171
+ .split(/\s+/)
172
+ .filter(Boolean)
173
+ .map((token) => token.replace(/"/g, ""))
174
+ .join(" OR ");
175
+ }
176
+
177
+ function escapeSql(value) {
178
+ return String(value).replace(/'/g, "''");
179
+ }
180
+
181
+ function dedupeQmdItems(items) {
182
+ const seen = new Set();
183
+ const deduped = [];
184
+
185
+ for (const item of items) {
186
+ const key = buildDedupKey(item);
187
+ if (seen.has(key)) continue;
188
+ seen.add(key);
189
+ deduped.push(item);
190
+ }
191
+
192
+ return deduped;
193
+ }
194
+
195
+ function buildDedupKey(item) {
196
+ const pathKey = normalizePath(item.document_path || item.filepath || "");
197
+ const titleKey = String(item.title || "").trim().toLowerCase();
198
+ const snippetKey = String(item.snippet || "")
199
+ .replace(/\s+/g, " ")
200
+ .trim()
201
+ .toLowerCase();
202
+
203
+ return [pathKey, titleKey, snippetKey].join("::");
204
+ }
205
+
206
+ function normalizePath(value) {
207
+ return String(value || "")
208
+ .replace(/\\/g, "/")
209
+ .trim()
210
+ .toLowerCase();
211
+ }
212
+
213
+ function matchesWorkspaceCollection(workspaceRoot, collectionPath) {
214
+ const normalizedWorkspace = normalizePath(workspaceRoot);
215
+ const normalizedCollection = normalizePath(collectionPath);
216
+ if (!normalizedWorkspace || !normalizedCollection) return false;
217
+ if (normalizedCollection === normalizedWorkspace) return true;
218
+ return normalizedCollection === `${normalizedWorkspace}/memory`;
219
+ }
@@ -0,0 +1,24 @@
1
+ function readFlag(name) {
2
+ const raw = process.env[name];
3
+ return raw === "1" || raw === "true";
4
+ }
5
+
6
+ export function getEmbeddingPolicy() {
7
+ return {
8
+ allowEmbeddings: readFlag("NEMORIS_ALLOW_EMBEDDINGS"),
9
+ allowRemoteEmbeddings: readFlag("NEMORIS_ALLOW_REMOTE_EMBEDDINGS"),
10
+ requireHealthyProvider: true,
11
+ allowedProvider: "ollama"
12
+ };
13
+ }
14
+
15
+ export function assertEmbeddingsAllowed(providerId) {
16
+ const policy = getEmbeddingPolicy();
17
+ if (!policy.allowEmbeddings) {
18
+ throw new Error("Embeddings disabled. Set NEMORIS_ALLOW_EMBEDDINGS=1 to enable embedding indexing.");
19
+ }
20
+ if (providerId !== policy.allowedProvider && !policy.allowRemoteEmbeddings) {
21
+ throw new Error(`Embedding provider ${providerId} is blocked by policy.`);
22
+ }
23
+ return policy;
24
+ }
@@ -0,0 +1,118 @@
1
+ import path from "node:path";
2
+ import { ensureDir } from "../utils/fs.js";
3
+ import { buildResultSignature } from "./memory-signature.js";
4
+ import { SqliteActiveStore } from "./sqlite-active-store.js";
5
+
6
+ export class EmbeddingIndex {
7
+ constructor({ rootDir, embeddingService = null } = {}) {
8
+ this.rootDir = rootDir;
9
+ this.embeddingService = embeddingService;
10
+ this.stores = new Map();
11
+ }
12
+
13
+ dbPath(agentId) {
14
+ return path.join(this.rootDir, agentId, "memory.sqlite");
15
+ }
16
+
17
+ async getStore(agentId) {
18
+ const dbPath = this.dbPath(agentId);
19
+ await ensureDir(path.dirname(dbPath));
20
+ if (!this.stores.has(dbPath)) {
21
+ this.stores.set(dbPath, new SqliteActiveStore({ dbPath }));
22
+ }
23
+ return this.stores.get(dbPath);
24
+ }
25
+
26
+ async load(agentId) {
27
+ const store = await this.getStore(agentId);
28
+ return {
29
+ agentId,
30
+ ...store.getEmbeddingIndex()
31
+ };
32
+ }
33
+
34
+ async save(agentId, payload) {
35
+ const store = await this.getStore(agentId);
36
+ return {
37
+ agentId,
38
+ ...store.replaceEmbeddings(payload)
39
+ };
40
+ }
41
+
42
+ async rebuild(agentId, memoryItems) {
43
+ if (!this.embeddingService) {
44
+ throw new Error("No embedding service configured");
45
+ }
46
+
47
+ const texts = memoryItems.map((item) =>
48
+ [item.title, item.summary, item.content, item.reason, item.category].filter(Boolean).join("\n")
49
+ );
50
+ const embedded = await this.embeddingService.embedTexts(texts);
51
+ const items = memoryItems.map((item, index) => ({
52
+ entryId: item.entryId || `synthetic:${index}:${buildResultSignature(item)}`,
53
+ signature: buildResultSignature(item),
54
+ item,
55
+ vector: embedded.vectors[index] || []
56
+ }));
57
+
58
+ return this.save(agentId, {
59
+ agentId,
60
+ updatedAt: new Date().toISOString(),
61
+ config: embedded.config,
62
+ items
63
+ });
64
+ }
65
+
66
+ async query(agentId, queryText, limit = 8) {
67
+ const store = await this.getStore(agentId);
68
+ const index = store.getEmbeddingIndex();
69
+ if (!this.embeddingService || !index.items?.length) {
70
+ return [];
71
+ }
72
+
73
+ const embedded = await this.embedQuery(queryText);
74
+ const queryVector = embedded.vector;
75
+ if (!Array.isArray(queryVector)) return [];
76
+
77
+ const sqliteResults = store.queryEmbeddingSimilarities(queryVector, limit);
78
+ if (sqliteResults.length) {
79
+ return sqliteResults;
80
+ }
81
+
82
+ return index.items
83
+ .map((entry) => ({
84
+ entryId: entry.entryId || null,
85
+ signature: entry.signature,
86
+ similarity: cosineSimilarity(queryVector, entry.vector || []),
87
+ freshnessStatus: entry.freshnessStatus || "fresh",
88
+ item: entry.item
89
+ }))
90
+ .sort((a, b) => b.similarity - a.similarity)
91
+ .slice(0, limit);
92
+ }
93
+
94
+ async embedQuery(queryText) {
95
+ if (!this.embeddingService) {
96
+ throw new Error("No embedding service configured");
97
+ }
98
+ const embedded = await this.embeddingService.embedTexts([queryText]);
99
+ return {
100
+ vector: embedded.vectors[0],
101
+ config: embedded.config || null
102
+ };
103
+ }
104
+ }
105
+
106
+ function cosineSimilarity(a = [], b = []) {
107
+ let dot = 0;
108
+ let normA = 0;
109
+ let normB = 0;
110
+ const length = Math.min(a.length, b.length);
111
+ for (let i = 0; i < length; i += 1) {
112
+ dot += a[i] * b[i];
113
+ normA += a[i] * a[i];
114
+ normB += b[i] * b[i];
115
+ }
116
+ if (normA === 0 || normB === 0) return 0;
117
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
118
+ }
@@ -0,0 +1,179 @@
1
+ import path from "node:path";
2
+ import { ConfigLoader } from "../config/loader.js";
3
+ import { ProviderRegistry } from "../providers/registry.js";
4
+ import { assertEmbeddingsAllowed, getEmbeddingPolicy } from "./embedding-guards.js";
5
+
6
+ function modelToProviderId(modelId) {
7
+ return String(modelId || "").split("/")[0] || null;
8
+ }
9
+
10
+ function normalizeOllamaModelName(modelId) {
11
+ const normalized = String(modelId || "").replace(/^ollama\//, "");
12
+ return normalized;
13
+ }
14
+
15
+ function matchesAvailableModel(targetModel, availableModels) {
16
+ const target = normalizeOllamaModelName(targetModel);
17
+ if (!target || !Array.isArray(availableModels)) return false;
18
+ return availableModels.some((model) => {
19
+ const candidate = normalizeOllamaModelName(model);
20
+ return candidate === target || candidate === `${target}:latest`;
21
+ });
22
+ }
23
+
24
+ export class EmbeddingService {
25
+ constructor({ projectRoot, fetchImpl } = {}) {
26
+ this.projectRoot = projectRoot;
27
+ this.loader = new ConfigLoader({ rootDir: path.join(projectRoot, "config") });
28
+ this.registry = new ProviderRegistry({ fetchImpl });
29
+ }
30
+
31
+ async loadConfig() {
32
+ const raw = await this.loader.loadEmbeddings();
33
+ return {
34
+ enabled: raw.enabled ?? false,
35
+ provider: raw.provider || modelToProviderId(raw.model),
36
+ model: raw.model || null,
37
+ dimensions: raw.dimensions ?? 128,
38
+ indexOnWrite: raw.indexOnWrite ?? false
39
+ };
40
+ }
41
+
42
+ async embedTexts(texts) {
43
+ const config = await this.loadConfig();
44
+ const providerId = config.provider || modelToProviderId(config.model);
45
+ const policy = assertEmbeddingsAllowed(providerId);
46
+ const providers = await this.loader.loadProviders();
47
+ const providerConfig = providers[providerId];
48
+ if (!providerConfig) {
49
+ throw new Error(`No provider config for embedding provider ${providerId}`);
50
+ }
51
+
52
+ const adapter = this.registry.create(providerConfig);
53
+ if (policy.requireHealthyProvider) {
54
+ const health = await adapter.healthCheck();
55
+ if (!health.ok) {
56
+ throw new Error(`Embedding provider health check failed for ${providerId} with status ${health.status}`);
57
+ }
58
+ }
59
+ if (typeof adapter.embed !== "function") {
60
+ throw new Error(`Provider ${providerId} does not implement embeddings`);
61
+ }
62
+
63
+ const result = await adapter.embed({
64
+ model: config.model,
65
+ input: texts
66
+ });
67
+
68
+ return {
69
+ config,
70
+ policy: getEmbeddingPolicy(),
71
+ vectors: result.embeddings || []
72
+ };
73
+ }
74
+
75
+ async getReadiness() {
76
+ const config = await this.loadConfig();
77
+ const providerId = config.provider || modelToProviderId(config.model);
78
+ const policy = getEmbeddingPolicy();
79
+
80
+ if (!policy.allowEmbeddings) {
81
+ return {
82
+ ready: false,
83
+ config,
84
+ policy,
85
+ providerId,
86
+ reason: "Embeddings disabled. Set NEMORIS_ALLOW_EMBEDDINGS=1 to enable embedding indexing."
87
+ };
88
+ }
89
+
90
+ if (providerId !== policy.allowedProvider && !policy.allowRemoteEmbeddings) {
91
+ return {
92
+ ready: false,
93
+ config,
94
+ policy,
95
+ providerId,
96
+ reason: `Embedding provider ${providerId} is blocked by policy.`
97
+ };
98
+ }
99
+
100
+ const providers = await this.loader.loadProviders();
101
+ const providerConfig = providers[providerId];
102
+ if (!providerConfig) {
103
+ return {
104
+ ready: false,
105
+ config,
106
+ policy,
107
+ providerId,
108
+ reason: `No provider config for embedding provider ${providerId}`
109
+ };
110
+ }
111
+
112
+ const adapter = this.registry.create(providerConfig);
113
+ const health = await adapter.healthCheck();
114
+ if (!health.ok) {
115
+ return {
116
+ ready: false,
117
+ config,
118
+ policy,
119
+ providerId,
120
+ providerHealth: health,
121
+ reason: `Embedding provider health check failed for ${providerId} with status ${health.status}`
122
+ };
123
+ }
124
+
125
+ if (typeof adapter.embed !== "function") {
126
+ return {
127
+ ready: false,
128
+ config,
129
+ policy,
130
+ providerId,
131
+ providerHealth: health,
132
+ reason: `Provider ${providerId} does not implement embeddings`
133
+ };
134
+ }
135
+
136
+ let availableModels = null;
137
+ let modelAvailable = null;
138
+ if (typeof adapter.listModels === "function") {
139
+ try {
140
+ availableModels = await adapter.listModels();
141
+ if (Array.isArray(availableModels) && config.model) {
142
+ const normalizedTarget = normalizeOllamaModelName(config.model);
143
+ modelAvailable = matchesAvailableModel(config.model, availableModels);
144
+ if (!modelAvailable) {
145
+ return {
146
+ ready: false,
147
+ config,
148
+ policy,
149
+ providerId,
150
+ providerHealth: health,
151
+ availableModels,
152
+ modelAvailable,
153
+ reason: `Embedding model ${normalizedTarget} is not available in ${providerId}`
154
+ };
155
+ }
156
+ }
157
+ } catch (error) {
158
+ return {
159
+ ready: false,
160
+ config,
161
+ policy,
162
+ providerId,
163
+ providerHealth: health,
164
+ reason: error?.message || String(error)
165
+ };
166
+ }
167
+ }
168
+
169
+ return {
170
+ ready: true,
171
+ config,
172
+ policy,
173
+ providerId,
174
+ providerHealth: health,
175
+ availableModels,
176
+ modelAvailable
177
+ };
178
+ }
179
+ }