nemoris 0.1.0 → 0.1.2
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/.env.example +49 -49
- package/LICENSE +21 -21
- package/README.md +209 -209
- package/SECURITY.md +59 -119
- package/bin/nemoris +46 -46
- package/config/agents/agent.toml.example +28 -28
- package/config/agents/content.toml +23 -0
- package/config/agents/default.toml +22 -22
- package/config/agents/heartbeat.toml +35 -0
- package/config/agents/iris.toml +23 -0
- package/config/agents/lab.toml +23 -0
- package/config/agents/main.toml +45 -0
- package/config/agents/nemo.toml +21 -0
- package/config/agents/ops.toml +38 -0
- package/config/agents/orchestrator.toml +18 -18
- package/config/agents/revenue.toml +23 -0
- package/config/agents/testyboo.toml +19 -0
- package/config/delivery.toml +73 -73
- package/config/embeddings.toml +5 -5
- package/config/identity/content-purpose.md +11 -0
- package/config/identity/content-soul.md +45 -0
- package/config/identity/default-purpose.md +1 -1
- package/config/identity/default-soul.md +3 -3
- package/config/identity/heartbeat-purpose.md +9 -0
- package/config/identity/heartbeat-soul.md +16 -0
- package/config/identity/iris-purpose.md +17 -0
- package/config/identity/iris-soul.md +68 -0
- package/config/identity/lab-purpose.md +10 -0
- package/config/identity/lab-soul.md +38 -0
- package/config/identity/main-purpose.md +17 -0
- package/config/identity/main-soul.md +66 -0
- package/config/identity/main-user.md +22 -0
- package/config/identity/ops-purpose.md +9 -0
- package/config/identity/ops-soul.md +16 -0
- package/config/identity/orchestrator-purpose.md +1 -1
- package/config/identity/orchestrator-soul.md +1 -1
- package/config/identity/revenue-purpose.md +9 -0
- package/config/identity/revenue-soul.md +41 -0
- package/config/identity/testyboo-purpose.md +13 -0
- package/config/identity/testyboo-soul.md +20 -0
- package/config/improvement-targets.toml +15 -15
- package/config/jobs/heartbeat-check.toml +30 -30
- package/config/jobs/memory-rollup.toml +46 -46
- package/config/jobs/workspace-health.toml +63 -63
- package/config/mcp.toml +16 -16
- package/config/output-contracts.toml +17 -17
- package/config/peers.toml +32 -32
- package/config/peers.toml.example +32 -32
- package/config/policies/memory-default.toml +10 -10
- package/config/policies/memory-heartbeat.toml +5 -5
- package/config/policies/memory-ops.toml +10 -10
- package/config/policies/tools-heartbeat-minimal.toml +8 -8
- package/config/policies/tools-interactive-safe.toml +8 -8
- package/config/policies/tools-ops-bounded.toml +8 -8
- package/config/policies/tools-orchestrator.toml +7 -7
- package/config/providers/anthropic.toml +15 -15
- package/config/providers/ollama.toml +5 -5
- package/config/providers/openai-codex.toml +9 -9
- package/config/providers/openrouter.toml +5 -5
- package/config/router.toml +22 -22
- package/config/runtime.toml +114 -114
- package/config/skills/self-improvement.toml +15 -15
- package/config/skills/telegram-onboarding-spec.md +240 -240
- package/config/skills/workspace-monitor.toml +15 -15
- package/config/task-router.toml +42 -42
- package/install.sh +50 -50
- package/package.json +91 -90
- package/src/auth/auth-profiles.js +169 -169
- package/src/auth/openai-codex-oauth.js +285 -285
- package/src/battle.js +449 -449
- package/src/cli/help.js +265 -265
- package/src/cli/output-filter.js +49 -49
- package/src/cli/runtime-control.js +704 -704
- package/src/cli-main.js +2763 -2763
- package/src/cli.js +78 -78
- package/src/config/loader.js +332 -332
- package/src/config/schema-validator.js +214 -214
- package/src/config/toml-lite.js +8 -8
- package/src/daemon/action-handlers.js +71 -71
- package/src/daemon/healing-tick.js +87 -87
- package/src/daemon/health-probes.js +90 -90
- package/src/daemon/notifier.js +57 -57
- package/src/daemon/nurse.js +218 -218
- package/src/daemon/repair-log.js +106 -106
- package/src/daemon/rule-staging.js +90 -90
- package/src/daemon/rules.js +29 -29
- package/src/daemon/telegram-commands.js +54 -54
- package/src/daemon/updater.js +85 -85
- package/src/jobs/job-runner.js +78 -78
- package/src/mcp/consumer.js +129 -129
- package/src/memory/active-recall.js +171 -171
- package/src/memory/backend-manager.js +97 -97
- package/src/memory/backends/file-backend.js +38 -38
- package/src/memory/backends/qmd-backend.js +219 -219
- package/src/memory/embedding-guards.js +24 -24
- package/src/memory/embedding-index.js +118 -118
- package/src/memory/embedding-service.js +179 -179
- package/src/memory/file-index.js +177 -177
- package/src/memory/memory-signature.js +5 -5
- package/src/memory/memory-store.js +648 -648
- package/src/memory/retrieval-planner.js +66 -66
- package/src/memory/scoring.js +145 -145
- package/src/memory/simhash.js +78 -78
- package/src/memory/sqlite-active-store.js +824 -824
- package/src/memory/write-policy.js +36 -36
- package/src/onboarding/aliases.js +33 -33
- package/src/onboarding/auth/api-key.js +224 -224
- package/src/onboarding/auth/ollama-detect.js +42 -42
- package/src/onboarding/clack-prompter.js +77 -77
- package/src/onboarding/doctor.js +530 -530
- package/src/onboarding/lock.js +42 -42
- package/src/onboarding/model-catalog.js +344 -344
- package/src/onboarding/phases/auth.js +576 -589
- package/src/onboarding/phases/build.js +130 -130
- package/src/onboarding/phases/choose.js +82 -82
- package/src/onboarding/phases/detect.js +98 -98
- package/src/onboarding/phases/hatch.js +216 -216
- package/src/onboarding/phases/identity.js +79 -79
- package/src/onboarding/phases/ollama.js +345 -345
- package/src/onboarding/phases/scaffold.js +99 -99
- package/src/onboarding/phases/telegram.js +377 -377
- package/src/onboarding/phases/validate.js +204 -204
- package/src/onboarding/phases/verify.js +206 -206
- package/src/onboarding/platform.js +482 -482
- package/src/onboarding/status-bar.js +95 -95
- package/src/onboarding/templates.js +794 -794
- package/src/onboarding/toml-writer.js +38 -38
- package/src/onboarding/tui.js +250 -250
- package/src/onboarding/uninstall.js +153 -153
- package/src/onboarding/wizard.js +516 -499
- package/src/providers/anthropic.js +168 -168
- package/src/providers/base.js +247 -247
- package/src/providers/circuit-breaker.js +136 -136
- package/src/providers/ollama.js +163 -163
- package/src/providers/openai-codex.js +149 -149
- package/src/providers/openrouter.js +136 -136
- package/src/providers/registry.js +36 -36
- package/src/providers/router.js +16 -16
- package/src/runtime/bootstrap-cache.js +47 -47
- package/src/runtime/capabilities-prompt.js +25 -25
- package/src/runtime/completion-ping.js +99 -99
- package/src/runtime/config-validator.js +121 -121
- package/src/runtime/context-ledger.js +360 -360
- package/src/runtime/cutover-readiness.js +42 -42
- package/src/runtime/daemon.js +729 -729
- package/src/runtime/delivery-ack.js +195 -195
- package/src/runtime/delivery-adapters/local-file.js +41 -41
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
- package/src/runtime/delivery-adapters/shadow.js +13 -13
- package/src/runtime/delivery-adapters/standalone-http.js +98 -98
- package/src/runtime/delivery-adapters/telegram.js +104 -104
- package/src/runtime/delivery-adapters/tui.js +128 -128
- package/src/runtime/delivery-manager.js +807 -807
- package/src/runtime/delivery-store.js +168 -168
- package/src/runtime/dependency-health.js +118 -118
- package/src/runtime/envelope.js +114 -114
- package/src/runtime/evaluation.js +1089 -1089
- package/src/runtime/exec-approvals.js +216 -216
- package/src/runtime/executor.js +500 -500
- package/src/runtime/failure-ping.js +67 -67
- package/src/runtime/flows.js +83 -83
- package/src/runtime/guards.js +45 -45
- package/src/runtime/handoff.js +51 -51
- package/src/runtime/identity-cache.js +28 -28
- package/src/runtime/improvement-engine.js +109 -109
- package/src/runtime/improvement-harness.js +581 -581
- package/src/runtime/input-sanitiser.js +72 -72
- package/src/runtime/interaction-contract.js +347 -347
- package/src/runtime/lane-readiness.js +226 -226
- package/src/runtime/migration.js +323 -323
- package/src/runtime/model-resolution.js +78 -78
- package/src/runtime/network.js +64 -64
- package/src/runtime/notification-store.js +97 -97
- package/src/runtime/notifier.js +256 -256
- package/src/runtime/orchestrator.js +53 -53
- package/src/runtime/orphan-reaper.js +41 -41
- package/src/runtime/output-contract-schema.js +139 -139
- package/src/runtime/output-contract-validator.js +439 -439
- package/src/runtime/peer-readiness.js +69 -69
- package/src/runtime/peer-registry.js +133 -133
- package/src/runtime/pilot-status.js +108 -108
- package/src/runtime/prompt-builder.js +261 -261
- package/src/runtime/provider-attempt.js +582 -582
- package/src/runtime/report-fallback.js +71 -71
- package/src/runtime/result-normalizer.js +183 -183
- package/src/runtime/retention.js +74 -74
- package/src/runtime/review.js +244 -244
- package/src/runtime/route-job.js +15 -15
- package/src/runtime/run-store.js +38 -38
- package/src/runtime/schedule.js +88 -88
- package/src/runtime/scheduler-state.js +434 -434
- package/src/runtime/scheduler.js +656 -656
- package/src/runtime/session-compactor.js +182 -182
- package/src/runtime/session-search.js +155 -155
- package/src/runtime/slack-inbound.js +249 -249
- package/src/runtime/ssrf.js +102 -102
- package/src/runtime/status-aggregator.js +330 -330
- package/src/runtime/task-contract.js +140 -140
- package/src/runtime/task-packet.js +107 -107
- package/src/runtime/task-router.js +140 -140
- package/src/runtime/telegram-inbound.js +1565 -1565
- package/src/runtime/token-counter.js +134 -134
- package/src/runtime/token-estimator.js +59 -59
- package/src/runtime/tool-loop.js +200 -200
- package/src/runtime/transport-server.js +311 -311
- package/src/runtime/tui-server.js +411 -411
- package/src/runtime/ulid.js +44 -44
- package/src/security/ssrf-check.js +197 -197
- package/src/setup.js +369 -369
- package/src/shadow/bridge.js +303 -303
- package/src/skills/loader.js +84 -84
- package/src/tools/catalog.json +49 -49
- package/src/tools/cli-delegate.js +44 -44
- package/src/tools/mcp-client.js +106 -106
- package/src/tools/micro/cancel-task.js +6 -6
- package/src/tools/micro/complete-task.js +6 -6
- package/src/tools/micro/fail-task.js +6 -6
- package/src/tools/micro/http-fetch.js +74 -74
- package/src/tools/micro/index.js +36 -36
- package/src/tools/micro/lcm-recall.js +60 -60
- package/src/tools/micro/list-dir.js +17 -17
- package/src/tools/micro/list-skills.js +46 -46
- package/src/tools/micro/load-skill.js +38 -38
- package/src/tools/micro/memory-search.js +45 -45
- package/src/tools/micro/read-file.js +11 -11
- package/src/tools/micro/session-search.js +54 -54
- package/src/tools/micro/shell-exec.js +43 -43
- package/src/tools/micro/trigger-job.js +79 -79
- package/src/tools/micro/web-search.js +58 -58
- package/src/tools/micro/workspace-paths.js +39 -39
- package/src/tools/micro/write-file.js +14 -14
- package/src/tools/micro/write-memory.js +41 -41
- package/src/tools/registry.js +348 -348
- package/src/tools/tool-result-contract.js +36 -36
- package/src/tui/chat.js +835 -835
- package/src/tui/renderer.js +175 -175
- package/src/tui/socket-client.js +217 -217
- package/src/utils/canonical-json.js +29 -29
- package/src/utils/compaction.js +30 -30
- package/src/utils/env-loader.js +5 -5
- package/src/utils/errors.js +80 -80
- package/src/utils/fs.js +101 -101
- package/src/utils/ids.js +5 -5
- package/src/utils/model-context-limits.js +30 -30
- package/src/utils/token-budget.js +74 -74
- package/src/utils/usage-cost.js +25 -25
- package/src/utils/usage-metrics.js +14 -14
|
@@ -1,219 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,24 +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
|
-
}
|
|
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
|
+
}
|