eve-knowledge 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.
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +1090 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +287 -0
- package/dist/index.js +1135 -0
- package/dist/index.js.map +1 -0
- package/docs/production-storage.md +92 -0
- package/examples/basic-eve-agent/agent/instructions.md +2 -0
- package/examples/basic-eve-agent/agent/knowledge/product/refunds.md +7 -0
- package/examples/basic-eve-agent/agent/tools/eve-knowledge.d.ts +8 -0
- package/examples/basic-eve-agent/agent/tools/search_knowledge.ts +37 -0
- package/examples/basic-eve-agent/eve-knowledge.config.json +4 -0
- package/examples/basic-eve-agent/package.json +13 -0
- package/examples/basic-eve-agent/production-store-recipe.ts +85 -0
- package/examples/basic-eve-agent/tsconfig.json +15 -0
- package/package.json +76 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
import path from "path";
|
|
5
|
+
var defaultIncludePatterns = ["**/*.{md,mdx,txt,json,yaml,yml}"];
|
|
6
|
+
var defaultIgnorePatterns = [
|
|
7
|
+
".git/**",
|
|
8
|
+
"node_modules/**",
|
|
9
|
+
"dist/**",
|
|
10
|
+
"build/**",
|
|
11
|
+
".next/**",
|
|
12
|
+
".nuxt/**",
|
|
13
|
+
".output/**",
|
|
14
|
+
".vercel/**",
|
|
15
|
+
".turbo/**",
|
|
16
|
+
".cache/**",
|
|
17
|
+
".eve-knowledge/**",
|
|
18
|
+
"**/.env",
|
|
19
|
+
"**/.env.*",
|
|
20
|
+
"**/*.pem",
|
|
21
|
+
"**/*.key",
|
|
22
|
+
"**/*secret*",
|
|
23
|
+
"**/*credential*"
|
|
24
|
+
];
|
|
25
|
+
var defaultMaxFileBytes = 256 * 1024;
|
|
26
|
+
var defaultChunking = {
|
|
27
|
+
maxCharacters: 1600,
|
|
28
|
+
overlapCharacters: 160
|
|
29
|
+
};
|
|
30
|
+
function defineKnowledgeConfig(config) {
|
|
31
|
+
return config;
|
|
32
|
+
}
|
|
33
|
+
function resolveKnowledgeConfig(config = {}, cwd = process.cwd()) {
|
|
34
|
+
const rootDir = path.resolve(cwd, config.rootDir ?? ".");
|
|
35
|
+
const agentDir = path.resolve(rootDir, config.agentDir ?? "agent");
|
|
36
|
+
const knowledgeDir = path.resolve(rootDir, config.knowledgeDir ?? "agent/knowledge");
|
|
37
|
+
const storeDir = path.resolve(rootDir, config.storeDir ?? ".eve-knowledge");
|
|
38
|
+
const chunking = {
|
|
39
|
+
...defaultChunking,
|
|
40
|
+
...config.chunking
|
|
41
|
+
};
|
|
42
|
+
if (chunking.maxCharacters <= 0) {
|
|
43
|
+
throw new Error("chunking.maxCharacters must be greater than 0");
|
|
44
|
+
}
|
|
45
|
+
if (chunking.overlapCharacters < 0) {
|
|
46
|
+
throw new Error("chunking.overlapCharacters must be zero or greater");
|
|
47
|
+
}
|
|
48
|
+
if (chunking.overlapCharacters >= chunking.maxCharacters) {
|
|
49
|
+
throw new Error("chunking.overlapCharacters must be smaller than chunking.maxCharacters");
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
rootDir,
|
|
53
|
+
agentDir,
|
|
54
|
+
knowledgeDir,
|
|
55
|
+
storeDir,
|
|
56
|
+
include: config.include ?? defaultIncludePatterns,
|
|
57
|
+
ignore: [...defaultIgnorePatterns, ...config.ignore ?? []],
|
|
58
|
+
maxFileBytes: config.maxFileBytes ?? defaultMaxFileBytes,
|
|
59
|
+
chunking,
|
|
60
|
+
redaction: {
|
|
61
|
+
mode: config.redaction?.mode ?? "warn"
|
|
62
|
+
},
|
|
63
|
+
memory: config.memory ?? { enabled: false }
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/indexer.ts
|
|
68
|
+
import { performance } from "perf_hooks";
|
|
69
|
+
|
|
70
|
+
// src/hash.ts
|
|
71
|
+
import { createHash } from "crypto";
|
|
72
|
+
function sha256(input) {
|
|
73
|
+
return createHash("sha256").update(input).digest("hex");
|
|
74
|
+
}
|
|
75
|
+
function shortHash(input, length = 16) {
|
|
76
|
+
return sha256(input).slice(0, length);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/chunk.ts
|
|
80
|
+
function chunkDocument(document, config, indexedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
81
|
+
const chunks = [];
|
|
82
|
+
for (const section of document.sections) {
|
|
83
|
+
const parts = splitText(section.text, config.chunking.maxCharacters, config.chunking.overlapCharacters);
|
|
84
|
+
parts.forEach((text, partIndex) => {
|
|
85
|
+
const contentHash = shortHash(text, 32);
|
|
86
|
+
const ordinal = chunks.length;
|
|
87
|
+
chunks.push({
|
|
88
|
+
id: createChunkId(document.source.path, section.headingPath, section.ordinal, partIndex, contentHash),
|
|
89
|
+
source: document.source,
|
|
90
|
+
text,
|
|
91
|
+
headingPath: section.headingPath,
|
|
92
|
+
ordinal,
|
|
93
|
+
contentHash,
|
|
94
|
+
tokenCount: estimateTokenCount(text),
|
|
95
|
+
charCount: text.length,
|
|
96
|
+
indexedAt
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return chunks;
|
|
101
|
+
}
|
|
102
|
+
function createChunkId(sourcePath, headingPath, sectionOrdinal, partOrdinal, contentHash) {
|
|
103
|
+
return `chk_${shortHash(
|
|
104
|
+
[sourcePath, headingPath.join(" > "), sectionOrdinal, partOrdinal, contentHash].join("\n"),
|
|
105
|
+
24
|
|
106
|
+
)}`;
|
|
107
|
+
}
|
|
108
|
+
function splitText(text, maxCharacters, overlapCharacters) {
|
|
109
|
+
const normalized = text.trim();
|
|
110
|
+
if (!normalized) return [];
|
|
111
|
+
if (normalized.length <= maxCharacters) return [normalized];
|
|
112
|
+
const parts = [];
|
|
113
|
+
let start = 0;
|
|
114
|
+
while (start < normalized.length) {
|
|
115
|
+
const hardEnd = Math.min(start + maxCharacters, normalized.length);
|
|
116
|
+
const end = chooseBreak(normalized, start, hardEnd);
|
|
117
|
+
const part = normalized.slice(start, end).trim();
|
|
118
|
+
if (part) parts.push(part);
|
|
119
|
+
if (end >= normalized.length) break;
|
|
120
|
+
start = Math.max(end - overlapCharacters, start + 1);
|
|
121
|
+
}
|
|
122
|
+
return parts;
|
|
123
|
+
}
|
|
124
|
+
function chooseBreak(text, start, hardEnd) {
|
|
125
|
+
if (hardEnd >= text.length) return text.length;
|
|
126
|
+
const window = text.slice(start, hardEnd);
|
|
127
|
+
const paragraphBreak = window.lastIndexOf("\n\n");
|
|
128
|
+
if (paragraphBreak > Math.floor(window.length * 0.5)) {
|
|
129
|
+
return start + paragraphBreak;
|
|
130
|
+
}
|
|
131
|
+
const sentenceBreak = Math.max(window.lastIndexOf(". "), window.lastIndexOf("? "), window.lastIndexOf("! "));
|
|
132
|
+
if (sentenceBreak > Math.floor(window.length * 0.5)) {
|
|
133
|
+
return start + sentenceBreak + 1;
|
|
134
|
+
}
|
|
135
|
+
const wordBreak = window.lastIndexOf(" ");
|
|
136
|
+
if (wordBreak > Math.floor(window.length * 0.5)) {
|
|
137
|
+
return start + wordBreak;
|
|
138
|
+
}
|
|
139
|
+
return hardEnd;
|
|
140
|
+
}
|
|
141
|
+
function estimateTokenCount(text) {
|
|
142
|
+
return Math.ceil(text.length / 4);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/loader.ts
|
|
146
|
+
import fs2 from "fs/promises";
|
|
147
|
+
import path4 from "path";
|
|
148
|
+
|
|
149
|
+
// src/fs-utils.ts
|
|
150
|
+
import fs from "fs/promises";
|
|
151
|
+
import path2 from "path";
|
|
152
|
+
function toRepoPath(filePath) {
|
|
153
|
+
return filePath.split(path2.sep).join("/");
|
|
154
|
+
}
|
|
155
|
+
function isMissingFileError(error) {
|
|
156
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
157
|
+
}
|
|
158
|
+
async function pathExists(filePath) {
|
|
159
|
+
try {
|
|
160
|
+
await fs.access(filePath);
|
|
161
|
+
return true;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/loader.ts
|
|
168
|
+
import { parse as parseYaml2 } from "yaml";
|
|
169
|
+
|
|
170
|
+
// src/frontmatter.ts
|
|
171
|
+
import { parse as parseYaml } from "yaml";
|
|
172
|
+
var frontmatterPattern = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
173
|
+
function parseFrontmatter(content) {
|
|
174
|
+
const match = frontmatterPattern.exec(content);
|
|
175
|
+
if (!match) {
|
|
176
|
+
return { body: content, metadata: {} };
|
|
177
|
+
}
|
|
178
|
+
const yamlText = match[1] ?? "";
|
|
179
|
+
const parsed = parseYaml(yamlText);
|
|
180
|
+
return {
|
|
181
|
+
body: content.slice(match[0].length),
|
|
182
|
+
metadata: normalizeMetadata(parsed)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function normalizeMetadata(value) {
|
|
186
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
187
|
+
return {};
|
|
188
|
+
}
|
|
189
|
+
const metadata = {};
|
|
190
|
+
for (const [key, rawValue] of Object.entries(value)) {
|
|
191
|
+
const normalized = normalizeMetadataValue(rawValue);
|
|
192
|
+
if (normalized !== void 0) {
|
|
193
|
+
metadata[key] = normalized;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return metadata;
|
|
197
|
+
}
|
|
198
|
+
function normalizeMetadataValue(value) {
|
|
199
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
if (Array.isArray(value)) {
|
|
203
|
+
const normalized = value.filter(isMetadataPrimitive);
|
|
204
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
205
|
+
}
|
|
206
|
+
return void 0;
|
|
207
|
+
}
|
|
208
|
+
function isMetadataPrimitive(value) {
|
|
209
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/format.ts
|
|
213
|
+
import path3 from "path";
|
|
214
|
+
function detectKnowledgeFormat(filePath) {
|
|
215
|
+
const ext = path3.extname(filePath).toLowerCase();
|
|
216
|
+
if (ext === ".md") return "markdown";
|
|
217
|
+
if (ext === ".mdx") return "mdx";
|
|
218
|
+
if (ext === ".txt") return "text";
|
|
219
|
+
if (ext === ".json") return "json";
|
|
220
|
+
if (ext === ".yaml" || ext === ".yml") return "yaml";
|
|
221
|
+
return void 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/redaction.ts
|
|
225
|
+
var KnowledgeRedactionError = class extends Error {
|
|
226
|
+
constructor(level, path10, findings) {
|
|
227
|
+
super(`Possible secret detected (${findings.map((finding) => finding.label).join(", ")}).`);
|
|
228
|
+
this.level = level;
|
|
229
|
+
this.path = path10;
|
|
230
|
+
this.findings = findings;
|
|
231
|
+
this.name = "KnowledgeRedactionError";
|
|
232
|
+
}
|
|
233
|
+
level;
|
|
234
|
+
path;
|
|
235
|
+
findings;
|
|
236
|
+
};
|
|
237
|
+
var secretPatterns = [
|
|
238
|
+
{ label: "openai_api_key", pattern: /\bsk-[A-Za-z0-9_-]{20,}\b/g },
|
|
239
|
+
{ label: "anthropic_api_key", pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
|
|
240
|
+
{ label: "github_token", pattern: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
|
|
241
|
+
{ label: "private_key", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g },
|
|
242
|
+
{
|
|
243
|
+
label: "env_secret_assignment",
|
|
244
|
+
pattern: /\b[A-Z0-9_]*(?:SECRET|TOKEN|API_KEY|PASSWORD)[A-Z0-9_]*\s*=\s*["']?[^"'\s]{8,}/g
|
|
245
|
+
}
|
|
246
|
+
];
|
|
247
|
+
function detectSecrets(content) {
|
|
248
|
+
const findings = [];
|
|
249
|
+
for (const { label, pattern } of secretPatterns) {
|
|
250
|
+
pattern.lastIndex = 0;
|
|
251
|
+
for (const match of content.matchAll(pattern)) {
|
|
252
|
+
findings.push({
|
|
253
|
+
label,
|
|
254
|
+
index: match.index ?? 0,
|
|
255
|
+
preview: maskSecret(match[0] ?? "")
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return findings;
|
|
260
|
+
}
|
|
261
|
+
function maskSecret(value) {
|
|
262
|
+
if (value.length <= 8) return "[redacted]";
|
|
263
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/sections.ts
|
|
267
|
+
var markdownHeadingPattern = /^(#{1,6})\s+(.+?)\s*#*\s*$/;
|
|
268
|
+
function splitMarkdownSections(content) {
|
|
269
|
+
const sections = [];
|
|
270
|
+
const headingStack = [];
|
|
271
|
+
let currentLines = [];
|
|
272
|
+
let currentHeadingPath = [];
|
|
273
|
+
let ordinal = 0;
|
|
274
|
+
for (const line of content.split(/\r?\n/)) {
|
|
275
|
+
const heading = markdownHeadingPattern.exec(line);
|
|
276
|
+
if (heading) {
|
|
277
|
+
pushSection();
|
|
278
|
+
const level = heading[1]?.length ?? 1;
|
|
279
|
+
const title = normalizeHeadingTitle(heading[2] ?? "");
|
|
280
|
+
while (headingStack.length > 0 && headingStack[headingStack.length - 1].level >= level) {
|
|
281
|
+
headingStack.pop();
|
|
282
|
+
}
|
|
283
|
+
headingStack.push({ level, title });
|
|
284
|
+
currentHeadingPath = headingStack.map((entry) => entry.title);
|
|
285
|
+
currentLines = [];
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
currentLines.push(line);
|
|
289
|
+
}
|
|
290
|
+
pushSection();
|
|
291
|
+
if (sections.length === 0) {
|
|
292
|
+
return [{ text: "", headingPath: [], ordinal: 0 }];
|
|
293
|
+
}
|
|
294
|
+
return sections;
|
|
295
|
+
function pushSection() {
|
|
296
|
+
const text = currentLines.join("\n").trim();
|
|
297
|
+
if (!text) {
|
|
298
|
+
currentLines = [];
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
sections.push({
|
|
302
|
+
text,
|
|
303
|
+
headingPath: currentHeadingPath,
|
|
304
|
+
ordinal
|
|
305
|
+
});
|
|
306
|
+
ordinal += 1;
|
|
307
|
+
currentLines = [];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function sectionFromText(content) {
|
|
311
|
+
return [
|
|
312
|
+
{
|
|
313
|
+
text: content.trim(),
|
|
314
|
+
headingPath: [],
|
|
315
|
+
ordinal: 0
|
|
316
|
+
}
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
function normalizeHeadingTitle(title) {
|
|
320
|
+
return title.replace(/\s+/g, " ").trim();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/loader.ts
|
|
324
|
+
async function loadKnowledgeDocument(filePath, config) {
|
|
325
|
+
const format = detectKnowledgeFormat(filePath);
|
|
326
|
+
if (!format) return void 0;
|
|
327
|
+
const stat = await fs2.stat(filePath);
|
|
328
|
+
const content = await fs2.readFile(filePath, "utf8");
|
|
329
|
+
const relativePath = toRepoPath(path4.relative(config.rootDir, filePath));
|
|
330
|
+
const secrets = detectSecrets(content);
|
|
331
|
+
if (secrets.length > 0 && config.redaction.mode !== "off") {
|
|
332
|
+
throw new KnowledgeRedactionError(
|
|
333
|
+
config.redaction.mode === "fail" ? "error" : "warning",
|
|
334
|
+
relativePath,
|
|
335
|
+
secrets
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
const contentHash = sha256(content);
|
|
339
|
+
const modifiedTime = stat.mtime.toISOString();
|
|
340
|
+
let metadata = {};
|
|
341
|
+
let body = content;
|
|
342
|
+
if (format === "markdown" || format === "mdx") {
|
|
343
|
+
const parsed = parseFrontmatter(content);
|
|
344
|
+
metadata = parsed.metadata;
|
|
345
|
+
body = parsed.body;
|
|
346
|
+
} else if (format === "json") {
|
|
347
|
+
const parsed = JSON.parse(content);
|
|
348
|
+
metadata = normalizeMetadata(parsed);
|
|
349
|
+
body = JSON.stringify(parsed, null, 2);
|
|
350
|
+
} else if (format === "yaml") {
|
|
351
|
+
const parsed = parseYaml2(content);
|
|
352
|
+
metadata = normalizeMetadata(parsed);
|
|
353
|
+
body = parsed === null || parsed === void 0 ? "" : JSON.stringify(parsed, null, 2);
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
source: {
|
|
357
|
+
path: relativePath,
|
|
358
|
+
format,
|
|
359
|
+
contentHash,
|
|
360
|
+
modifiedTime,
|
|
361
|
+
sizeBytes: stat.size,
|
|
362
|
+
metadata
|
|
363
|
+
},
|
|
364
|
+
sections: format === "markdown" || format === "mdx" ? splitMarkdownSections(body) : sectionFromText(body)
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/scan.ts
|
|
369
|
+
import fs3 from "fs/promises";
|
|
370
|
+
import path5 from "path";
|
|
371
|
+
import fg from "fast-glob";
|
|
372
|
+
import ignore from "ignore";
|
|
373
|
+
async function scanKnowledgeFiles(config) {
|
|
374
|
+
const builtInIg = ignore().add(config.ignore);
|
|
375
|
+
const userIg = ignore().add(await readEveKnowledgeIgnore(config));
|
|
376
|
+
const rootRealPath = await fs3.realpath(config.rootDir);
|
|
377
|
+
const knowledgeRealPath = await fs3.realpath(config.knowledgeDir);
|
|
378
|
+
const entries = await fg(config.include, {
|
|
379
|
+
cwd: config.knowledgeDir,
|
|
380
|
+
absolute: true,
|
|
381
|
+
onlyFiles: true,
|
|
382
|
+
dot: true,
|
|
383
|
+
followSymbolicLinks: false,
|
|
384
|
+
unique: true
|
|
385
|
+
});
|
|
386
|
+
const files = [];
|
|
387
|
+
const skipped = [];
|
|
388
|
+
const warnings = [];
|
|
389
|
+
const errors = [];
|
|
390
|
+
for (const filePath of entries.sort()) {
|
|
391
|
+
const relativeToRoot = toRepoPath(path5.relative(config.rootDir, filePath));
|
|
392
|
+
const relativeToKnowledge = toRepoPath(path5.relative(config.knowledgeDir, filePath));
|
|
393
|
+
const lstat = await fs3.lstat(filePath);
|
|
394
|
+
if (lstat.isSymbolicLink()) {
|
|
395
|
+
skipped.push(issue("warning", relativeToRoot, "symlink_skipped", "Symlinks are not indexed."));
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const realPath = await fs3.realpath(filePath);
|
|
399
|
+
if (!isInside(realPath, knowledgeRealPath) || !isInside(realPath, rootRealPath)) {
|
|
400
|
+
skipped.push(issue("warning", relativeToRoot, "path_escape", "File resolves outside the knowledge tree."));
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (builtInIg.ignores(relativeToRoot) || builtInIg.ignores(relativeToKnowledge) || userIg.ignores(relativeToRoot) || userIg.ignores(relativeToKnowledge)) {
|
|
404
|
+
skipped.push(issue("info", relativeToRoot, "ignored", "File matched .eveknowledgeignore rules."));
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const stat = await fs3.stat(filePath);
|
|
408
|
+
if (stat.size > config.maxFileBytes) {
|
|
409
|
+
skipped.push(
|
|
410
|
+
issue(
|
|
411
|
+
"warning",
|
|
412
|
+
relativeToRoot,
|
|
413
|
+
"max_file_size",
|
|
414
|
+
`File is ${stat.size} bytes, above the ${config.maxFileBytes} byte limit.`
|
|
415
|
+
)
|
|
416
|
+
);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
files.push(filePath);
|
|
420
|
+
}
|
|
421
|
+
return { files, skipped, warnings, errors };
|
|
422
|
+
}
|
|
423
|
+
async function readEveKnowledgeIgnore(config) {
|
|
424
|
+
const candidates = [
|
|
425
|
+
path5.join(config.rootDir, ".eveknowledgeignore"),
|
|
426
|
+
path5.join(config.knowledgeDir, ".eveknowledgeignore")
|
|
427
|
+
];
|
|
428
|
+
const patterns = [];
|
|
429
|
+
for (const filePath of candidates) {
|
|
430
|
+
try {
|
|
431
|
+
const content = await fs3.readFile(filePath, "utf8");
|
|
432
|
+
patterns.push(
|
|
433
|
+
...content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"))
|
|
434
|
+
);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
if (!isMissingFileError(error)) {
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return patterns;
|
|
442
|
+
}
|
|
443
|
+
function issue(level, filePath, code, message) {
|
|
444
|
+
return { level, path: filePath, code, message };
|
|
445
|
+
}
|
|
446
|
+
function isInside(candidate, directory) {
|
|
447
|
+
const relative = path5.relative(directory, candidate);
|
|
448
|
+
return relative === "" || !relative.startsWith("..") && !path5.isAbsolute(relative);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/store/local.ts
|
|
452
|
+
import fs4 from "fs/promises";
|
|
453
|
+
import path6 from "path";
|
|
454
|
+
|
|
455
|
+
// src/store/helpers.ts
|
|
456
|
+
function replaceChunksBySource(existing, incoming) {
|
|
457
|
+
const incomingSourcePaths = new Set(incoming.map((chunk) => chunk.source.path));
|
|
458
|
+
const incomingIds = new Set(incoming.map((chunk) => chunk.id));
|
|
459
|
+
return [
|
|
460
|
+
...existing.filter(
|
|
461
|
+
(chunk) => !incomingSourcePaths.has(chunk.source.path) && !incomingIds.has(chunk.id)
|
|
462
|
+
),
|
|
463
|
+
...incoming
|
|
464
|
+
].sort((a, b) => a.id.localeCompare(b.id));
|
|
465
|
+
}
|
|
466
|
+
function removeChunksBySource(chunks, sourcePath) {
|
|
467
|
+
return chunks.filter((chunk) => chunk.source.path !== sourcePath);
|
|
468
|
+
}
|
|
469
|
+
function citationForChunk(chunk) {
|
|
470
|
+
return {
|
|
471
|
+
path: chunk.source.path,
|
|
472
|
+
chunkId: chunk.id,
|
|
473
|
+
indexedAt: chunk.indexedAt,
|
|
474
|
+
...chunk.headingPath.length > 0 ? { heading: chunk.headingPath.join(" > ") } : {}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function countSources(chunks) {
|
|
478
|
+
return listSourcePaths(chunks).length;
|
|
479
|
+
}
|
|
480
|
+
function listSourcePaths(chunks) {
|
|
481
|
+
return [...new Set(chunks.map((chunk) => chunk.source.path))].sort();
|
|
482
|
+
}
|
|
483
|
+
function matchesMetadataFilters(chunk, filters) {
|
|
484
|
+
if (!filters) return true;
|
|
485
|
+
for (const [key, expected] of Object.entries(filters)) {
|
|
486
|
+
const actual = chunk.source.metadata[key];
|
|
487
|
+
if (actual === void 0) return false;
|
|
488
|
+
const expectedValues = Array.isArray(expected) ? expected : [expected];
|
|
489
|
+
const actualValues = Array.isArray(actual) ? actual : [actual];
|
|
490
|
+
if (!expectedValues.some((value) => actualValues.includes(value))) {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/store/local.ts
|
|
498
|
+
var LocalKnowledgeStore = class {
|
|
499
|
+
name = "local-json";
|
|
500
|
+
durability = "local";
|
|
501
|
+
filePath;
|
|
502
|
+
data;
|
|
503
|
+
constructor(options) {
|
|
504
|
+
this.filePath = path6.join(options.storeDir, "index.json");
|
|
505
|
+
}
|
|
506
|
+
async upsertChunks(chunks) {
|
|
507
|
+
const data = await this.read();
|
|
508
|
+
data.chunks = replaceChunksBySource(data.chunks, chunks);
|
|
509
|
+
await this.write(data);
|
|
510
|
+
}
|
|
511
|
+
async deleteBySource(sourcePath) {
|
|
512
|
+
const data = await this.read();
|
|
513
|
+
data.chunks = removeChunksBySource(data.chunks, sourcePath);
|
|
514
|
+
await this.write(data);
|
|
515
|
+
}
|
|
516
|
+
async search(input) {
|
|
517
|
+
const data = await this.read();
|
|
518
|
+
const queryTokens = tokenize(input.query);
|
|
519
|
+
if (queryTokens.length === 0) return [];
|
|
520
|
+
return data.chunks.filter((chunk) => matchesMetadataFilters(chunk, input.filters)).map((chunk) => ({
|
|
521
|
+
chunk,
|
|
522
|
+
score: scoreChunk(chunk, queryTokens),
|
|
523
|
+
citation: citationForChunk(chunk)
|
|
524
|
+
})).filter((hit) => hit.score > 0).sort((a, b) => b.score - a.score || a.chunk.source.path.localeCompare(b.chunk.source.path)).slice(0, input.topK);
|
|
525
|
+
}
|
|
526
|
+
async stats() {
|
|
527
|
+
const data = await this.read();
|
|
528
|
+
return {
|
|
529
|
+
chunks: data.chunks.length,
|
|
530
|
+
sources: countSources(data.chunks),
|
|
531
|
+
storePath: this.filePath,
|
|
532
|
+
durability: this.durability
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
async listChunks() {
|
|
536
|
+
const data = await this.read();
|
|
537
|
+
return [...data.chunks];
|
|
538
|
+
}
|
|
539
|
+
async listSources() {
|
|
540
|
+
const data = await this.read();
|
|
541
|
+
return listSourcePaths(data.chunks);
|
|
542
|
+
}
|
|
543
|
+
async read() {
|
|
544
|
+
if (this.data) return this.data;
|
|
545
|
+
try {
|
|
546
|
+
const content = await fs4.readFile(this.filePath, "utf8");
|
|
547
|
+
this.data = JSON.parse(content);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (!isMissingFileError(error)) throw error;
|
|
550
|
+
this.data = { version: 1, chunks: [] };
|
|
551
|
+
}
|
|
552
|
+
return this.data;
|
|
553
|
+
}
|
|
554
|
+
async write(data) {
|
|
555
|
+
await fs4.mkdir(path6.dirname(this.filePath), { recursive: true });
|
|
556
|
+
const tempPath = `${this.filePath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
557
|
+
await fs4.writeFile(tempPath, `${JSON.stringify(data, null, 2)}
|
|
558
|
+
`);
|
|
559
|
+
await fs4.rename(tempPath, this.filePath);
|
|
560
|
+
this.data = data;
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
function createLocalKnowledgeStore(options) {
|
|
564
|
+
return new LocalKnowledgeStore(options);
|
|
565
|
+
}
|
|
566
|
+
function scoreChunk(chunk, queryTokens) {
|
|
567
|
+
const haystack = tokenize(
|
|
568
|
+
[
|
|
569
|
+
chunk.text,
|
|
570
|
+
chunk.headingPath.join(" "),
|
|
571
|
+
chunk.source.path,
|
|
572
|
+
Object.values(chunk.source.metadata).flat().join(" ")
|
|
573
|
+
].join(" ")
|
|
574
|
+
);
|
|
575
|
+
const haystackCounts = /* @__PURE__ */ new Map();
|
|
576
|
+
for (const token of haystack) {
|
|
577
|
+
haystackCounts.set(token, (haystackCounts.get(token) ?? 0) + 1);
|
|
578
|
+
}
|
|
579
|
+
let score = 0;
|
|
580
|
+
for (const token of queryTokens) {
|
|
581
|
+
score += haystackCounts.get(token) ?? 0;
|
|
582
|
+
}
|
|
583
|
+
if (queryTokens.some((token) => chunk.source.path.toLowerCase().includes(token))) {
|
|
584
|
+
score += 0.25;
|
|
585
|
+
}
|
|
586
|
+
return score / queryTokens.length;
|
|
587
|
+
}
|
|
588
|
+
function tokenize(input) {
|
|
589
|
+
return input.toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length > 1);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/indexer.ts
|
|
593
|
+
async function indexKnowledge(options = {}) {
|
|
594
|
+
const startedAt = performance.now();
|
|
595
|
+
const config = resolveKnowledgeConfig(options.config, options.cwd);
|
|
596
|
+
const store = options.store ?? createLocalKnowledgeStore({ storeDir: config.storeDir });
|
|
597
|
+
const scan = await scanKnowledgeFiles(config);
|
|
598
|
+
const warnings = [...scan.warnings];
|
|
599
|
+
const errors = [...scan.errors];
|
|
600
|
+
if (errors.length > 0) {
|
|
601
|
+
return {
|
|
602
|
+
filesScanned: scan.files.length + scan.skipped.length + scan.errors.length,
|
|
603
|
+
filesIndexed: 0,
|
|
604
|
+
filesSkipped: scan.skipped.length,
|
|
605
|
+
chunksCreated: 0,
|
|
606
|
+
chunksReused: 0,
|
|
607
|
+
sourcesChanged: 0,
|
|
608
|
+
sourcesDeleted: 0,
|
|
609
|
+
warnings,
|
|
610
|
+
errors,
|
|
611
|
+
elapsedMs: Math.round(performance.now() - startedAt),
|
|
612
|
+
store: await store.stats()
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
const existingChunks = "listChunks" in store && store.listChunks ? await store.listChunks() : [];
|
|
616
|
+
const existingBySource = groupChunksBySource(existingChunks);
|
|
617
|
+
const seenSources = /* @__PURE__ */ new Set();
|
|
618
|
+
let chunksCreated = 0;
|
|
619
|
+
let chunksReused = 0;
|
|
620
|
+
let sourcesChanged = 0;
|
|
621
|
+
let sourcesDeleted = 0;
|
|
622
|
+
let filesIndexed = 0;
|
|
623
|
+
const chunksToUpsert = [];
|
|
624
|
+
const sourcesToDelete = [];
|
|
625
|
+
for (const filePath of scan.files) {
|
|
626
|
+
try {
|
|
627
|
+
const document = await loadKnowledgeDocument(filePath, config);
|
|
628
|
+
if (!document) continue;
|
|
629
|
+
const chunks = chunkDocument(document, config, options.now?.toISOString());
|
|
630
|
+
const existingForSource = existingBySource.get(document.source.path) ?? [];
|
|
631
|
+
const reconciled = reconcileChunks(chunks, existingForSource);
|
|
632
|
+
chunksReused += reconciled.reused;
|
|
633
|
+
chunksCreated += reconciled.created;
|
|
634
|
+
if (!options.dryRun) {
|
|
635
|
+
if (chunks.length === 0) {
|
|
636
|
+
sourcesToDelete.push(document.source.path);
|
|
637
|
+
} else if (!reconciled.unchanged) {
|
|
638
|
+
chunksToUpsert.push(...reconciled.chunks);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (!reconciled.unchanged) {
|
|
642
|
+
sourcesChanged += 1;
|
|
643
|
+
}
|
|
644
|
+
seenSources.add(document.source.path);
|
|
645
|
+
filesIndexed += 1;
|
|
646
|
+
} catch (error) {
|
|
647
|
+
if (error instanceof KnowledgeRedactionError) {
|
|
648
|
+
const issue2 = {
|
|
649
|
+
level: error.level,
|
|
650
|
+
path: error.path,
|
|
651
|
+
code: "possible_secret",
|
|
652
|
+
message: error.message
|
|
653
|
+
};
|
|
654
|
+
if (error.level === "error") errors.push(issue2);
|
|
655
|
+
else warnings.push(issue2);
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
errors.push({
|
|
659
|
+
level: "error",
|
|
660
|
+
path: filePath,
|
|
661
|
+
code: "load_failed",
|
|
662
|
+
message: error instanceof Error ? error.message : "Failed to load knowledge file."
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if ("listSources" in store && store.listSources) {
|
|
667
|
+
const previousSources = await store.listSources();
|
|
668
|
+
for (const sourcePath of previousSources.filter((sourcePath2) => !seenSources.has(sourcePath2))) {
|
|
669
|
+
sourcesDeleted += 1;
|
|
670
|
+
if (!options.dryRun) {
|
|
671
|
+
sourcesToDelete.push(sourcePath);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (!options.dryRun) {
|
|
676
|
+
if (chunksToUpsert.length > 0) {
|
|
677
|
+
await store.upsertChunks(chunksToUpsert);
|
|
678
|
+
}
|
|
679
|
+
for (const sourcePath of sourcesToDelete) {
|
|
680
|
+
await store.deleteBySource(sourcePath);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
filesScanned: scan.files.length + scan.skipped.length,
|
|
685
|
+
filesIndexed,
|
|
686
|
+
filesSkipped: scan.skipped.length + warnings.length,
|
|
687
|
+
chunksCreated,
|
|
688
|
+
chunksReused,
|
|
689
|
+
sourcesChanged,
|
|
690
|
+
sourcesDeleted,
|
|
691
|
+
warnings,
|
|
692
|
+
errors,
|
|
693
|
+
elapsedMs: Math.round(performance.now() - startedAt),
|
|
694
|
+
store: await store.stats()
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function reconcileChunks(chunks, existingChunks) {
|
|
698
|
+
const existingById = new Map(existingChunks.map((chunk) => [chunk.id, chunk]));
|
|
699
|
+
let reused = 0;
|
|
700
|
+
let created = 0;
|
|
701
|
+
const reconciled = chunks.map((chunk) => {
|
|
702
|
+
const existing = existingById.get(chunk.id);
|
|
703
|
+
if (existing?.contentHash === chunk.contentHash) {
|
|
704
|
+
reused += 1;
|
|
705
|
+
return mergeReusedChunk(chunk, existing);
|
|
706
|
+
}
|
|
707
|
+
created += 1;
|
|
708
|
+
return chunk;
|
|
709
|
+
});
|
|
710
|
+
return {
|
|
711
|
+
chunks: reconciled,
|
|
712
|
+
reused,
|
|
713
|
+
created,
|
|
714
|
+
unchanged: isSameChunkSet(reconciled, existingChunks)
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function mergeReusedChunk(chunk, existing) {
|
|
718
|
+
return {
|
|
719
|
+
...chunk,
|
|
720
|
+
source: sameSourceExceptModifiedTime(chunk, existing) ? existing.source : chunk.source,
|
|
721
|
+
indexedAt: existing.indexedAt
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function sameSourceExceptModifiedTime(next, existing) {
|
|
725
|
+
const { modifiedTime: _nextModifiedTime, ...nextSource } = next.source;
|
|
726
|
+
const { modifiedTime: _existingModifiedTime, ...existingSource } = existing.source;
|
|
727
|
+
return JSON.stringify(nextSource) === JSON.stringify(existingSource);
|
|
728
|
+
}
|
|
729
|
+
function isSameChunkSet(nextChunks, existingChunks) {
|
|
730
|
+
if (nextChunks.length !== existingChunks.length) return false;
|
|
731
|
+
const existingById = new Map(existingChunks.map((chunk) => [chunk.id, chunk]));
|
|
732
|
+
return nextChunks.every((chunk) => {
|
|
733
|
+
const existing = existingById.get(chunk.id);
|
|
734
|
+
return existing !== void 0 && JSON.stringify(existing) === JSON.stringify(chunk);
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
function groupChunksBySource(chunks) {
|
|
738
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
739
|
+
for (const chunk of chunks) {
|
|
740
|
+
const group = grouped.get(chunk.source.path) ?? [];
|
|
741
|
+
group.push(chunk);
|
|
742
|
+
grouped.set(chunk.source.path, group);
|
|
743
|
+
}
|
|
744
|
+
return grouped;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/check.ts
|
|
748
|
+
async function checkKnowledge(options = {}) {
|
|
749
|
+
const store = options.store;
|
|
750
|
+
const summary = await indexKnowledge({
|
|
751
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
752
|
+
config: {
|
|
753
|
+
...options.config,
|
|
754
|
+
redaction: {
|
|
755
|
+
...options.config?.redaction,
|
|
756
|
+
mode: "fail"
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
...store ? { store } : {},
|
|
760
|
+
dryRun: true
|
|
761
|
+
});
|
|
762
|
+
const issues = [...summary.errors];
|
|
763
|
+
if (summary.sourcesChanged > 0 || summary.sourcesDeleted > 0) {
|
|
764
|
+
issues.push({
|
|
765
|
+
level: "error",
|
|
766
|
+
code: "stale_index",
|
|
767
|
+
message: `${summary.sourcesChanged} sources changed and ${summary.sourcesDeleted} sources were deleted. Run eve-knowledge index before shipping.`
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
if (options.production) {
|
|
771
|
+
const durability = options.store?.durability ?? summary.store.durability;
|
|
772
|
+
if (durability !== "durable") {
|
|
773
|
+
issues.push({
|
|
774
|
+
level: "error",
|
|
775
|
+
code: "non_durable_store",
|
|
776
|
+
message: "Production checks require a durable KnowledgeStore. Local filesystem storage is not durable in serverless deployments."
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
ok: issues.length === 0,
|
|
782
|
+
summary,
|
|
783
|
+
issues
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/config-loader.ts
|
|
788
|
+
import fs5 from "fs/promises";
|
|
789
|
+
import path7 from "path";
|
|
790
|
+
import { pathToFileURL } from "url";
|
|
791
|
+
var safeConfigFileNames = ["eve-knowledge.config.json"];
|
|
792
|
+
var trustedConfigFileNames = ["eve-knowledge.config.ts", "eve-knowledge.config.mjs", "eve-knowledge.config.js"];
|
|
793
|
+
async function loadKnowledgeConfig(options = {}) {
|
|
794
|
+
const cwd = options.cwd ?? process.cwd();
|
|
795
|
+
const safeConfigPath = await findConfigPath(cwd, safeConfigFileNames);
|
|
796
|
+
if (safeConfigPath) {
|
|
797
|
+
return JSON.parse(await fs5.readFile(safeConfigPath, "utf8"));
|
|
798
|
+
}
|
|
799
|
+
const configPath = await findConfigPath(cwd, trustedConfigFileNames);
|
|
800
|
+
if (!configPath) return {};
|
|
801
|
+
if (!options.trustedConfig) {
|
|
802
|
+
throw new Error(
|
|
803
|
+
`Refusing to execute ${path7.basename(configPath)}. Use --trusted-config for trusted local config, or use eve-knowledge.config.json for CI.`
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
const loaded = await import(pathToFileURL(configPath).href);
|
|
807
|
+
return loaded.default ?? {};
|
|
808
|
+
}
|
|
809
|
+
async function findConfigPath(cwd, fileNames) {
|
|
810
|
+
for (const fileName of fileNames) {
|
|
811
|
+
const filePath = path7.join(cwd, fileName);
|
|
812
|
+
try {
|
|
813
|
+
await fs5.access(filePath);
|
|
814
|
+
return filePath;
|
|
815
|
+
} catch {
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return void 0;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// src/input.ts
|
|
822
|
+
import { z } from "zod";
|
|
823
|
+
var metadataPrimitiveSchema = z.union([z.string(), z.number(), z.boolean()]);
|
|
824
|
+
var searchKnowledgeInputSchema = z.object({
|
|
825
|
+
query: z.string().min(1),
|
|
826
|
+
topK: z.number().int().min(1).max(20).optional(),
|
|
827
|
+
filters: z.record(z.string(), z.union([metadataPrimitiveSchema, z.array(metadataPrimitiveSchema)])).optional()
|
|
828
|
+
}).strict();
|
|
829
|
+
function parseSearchKnowledgeInput(input) {
|
|
830
|
+
const parsed = searchKnowledgeInputSchema.parse(input);
|
|
831
|
+
return {
|
|
832
|
+
query: parsed.query,
|
|
833
|
+
...parsed.topK !== void 0 ? { topK: parsed.topK } : {},
|
|
834
|
+
...parsed.filters !== void 0 ? { filters: parsed.filters } : {}
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/search.ts
|
|
839
|
+
async function searchKnowledge(input, options = {}) {
|
|
840
|
+
const config = resolveKnowledgeConfig(options.config, options.cwd);
|
|
841
|
+
const store = options.store ?? createLocalKnowledgeStore({ storeDir: config.storeDir });
|
|
842
|
+
const topK = Math.min(Math.max(input.topK ?? options.maxResults ?? 5, 1), 20);
|
|
843
|
+
const results = await store.search({
|
|
844
|
+
query: input.query,
|
|
845
|
+
topK,
|
|
846
|
+
...input.filters ? { filters: input.filters } : {}
|
|
847
|
+
});
|
|
848
|
+
if (results.length === 0) {
|
|
849
|
+
return {
|
|
850
|
+
status: "no_results",
|
|
851
|
+
query: input.query,
|
|
852
|
+
message: "No relevant knowledge results were found. Do not fabricate an answer; ask for source material or say you do not know."
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
return {
|
|
856
|
+
status: "results",
|
|
857
|
+
query: input.query,
|
|
858
|
+
results
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// src/evals.ts
|
|
863
|
+
async function runKnowledgeEvals(options) {
|
|
864
|
+
await indexKnowledge({
|
|
865
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
866
|
+
...options.config ? { config: options.config } : {}
|
|
867
|
+
});
|
|
868
|
+
const results = [];
|
|
869
|
+
for (const testCase of options.cases) {
|
|
870
|
+
const response = await searchKnowledge(
|
|
871
|
+
{
|
|
872
|
+
query: testCase.query,
|
|
873
|
+
...testCase.filters ? { filters: testCase.filters } : {}
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
877
|
+
...options.config ? { config: options.config } : {}
|
|
878
|
+
}
|
|
879
|
+
);
|
|
880
|
+
if (testCase.expectNoResults) {
|
|
881
|
+
results.push({
|
|
882
|
+
name: testCase.name,
|
|
883
|
+
passed: response.status === "no_results",
|
|
884
|
+
message: response.status === "no_results" ? "No-result behavior matched." : "Expected no results but received cited results."
|
|
885
|
+
});
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
if (response.status === "no_results") {
|
|
889
|
+
results.push({
|
|
890
|
+
name: testCase.name,
|
|
891
|
+
passed: false,
|
|
892
|
+
message: "Expected cited results but received no_results."
|
|
893
|
+
});
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
const matched = testCase.expectPath ? response.results.some((result) => result.citation.path === testCase.expectPath) : response.results.length > 0;
|
|
897
|
+
results.push({
|
|
898
|
+
name: testCase.name,
|
|
899
|
+
passed: matched,
|
|
900
|
+
message: matched ? "Expected citation found." : `Missing citation ${testCase.expectPath}.`
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
return results;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// src/model-output.ts
|
|
907
|
+
function toModelOutput(output) {
|
|
908
|
+
if (output.status === "no_results") {
|
|
909
|
+
return {
|
|
910
|
+
type: "json",
|
|
911
|
+
value: {
|
|
912
|
+
status: output.status,
|
|
913
|
+
query: output.query,
|
|
914
|
+
message: output.message
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
return {
|
|
919
|
+
type: "json",
|
|
920
|
+
value: {
|
|
921
|
+
status: output.status,
|
|
922
|
+
query: output.query,
|
|
923
|
+
citations: output.results.map((result) => result.citation),
|
|
924
|
+
results: output.results.slice(0, 5).map((result) => ({
|
|
925
|
+
text: boundText(result.chunk.text, 800),
|
|
926
|
+
score: result.score,
|
|
927
|
+
citation: result.citation
|
|
928
|
+
}))
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
function boundText(text, maxCharacters) {
|
|
933
|
+
if (text.length <= maxCharacters) return text;
|
|
934
|
+
return `${text.slice(0, maxCharacters - 1).trimEnd()}...`;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/scaffold.ts
|
|
938
|
+
import fs7 from "fs/promises";
|
|
939
|
+
import path9 from "path";
|
|
940
|
+
|
|
941
|
+
// src/project.ts
|
|
942
|
+
import fs6 from "fs/promises";
|
|
943
|
+
import path8 from "path";
|
|
944
|
+
async function detectEveProject(cwd) {
|
|
945
|
+
const signals = [];
|
|
946
|
+
const agentDir = path8.join(cwd, "agent");
|
|
947
|
+
if (await pathExists(agentDir)) {
|
|
948
|
+
signals.push("agent/");
|
|
949
|
+
}
|
|
950
|
+
for (const filePath of ["agent/agent.ts", "agent/instructions.md", "agent/tools"]) {
|
|
951
|
+
if (await pathExists(path8.join(cwd, filePath))) {
|
|
952
|
+
signals.push(filePath);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (await packageHasEveDependency(path8.join(cwd, "package.json"))) {
|
|
956
|
+
signals.push("package.json:eve");
|
|
957
|
+
}
|
|
958
|
+
return {
|
|
959
|
+
isEveProject: signals.some((signal) => signal !== "agent/"),
|
|
960
|
+
signals
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
async function packageHasEveDependency(filePath) {
|
|
964
|
+
try {
|
|
965
|
+
const pkg = JSON.parse(await fs6.readFile(filePath, "utf8"));
|
|
966
|
+
return Boolean(pkg.dependencies?.eve ?? pkg.devDependencies?.eve);
|
|
967
|
+
} catch {
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// src/scaffold.ts
|
|
973
|
+
async function scaffoldEveKnowledge(options) {
|
|
974
|
+
const detection = await detectEveProject(options.cwd);
|
|
975
|
+
if (!detection.isEveProject && !options.allowNonEve) {
|
|
976
|
+
throw new Error(
|
|
977
|
+
"No Eve project detected. Run this inside an Eve app or pass --allow-non-eve to scaffold anyway."
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
const files = scaffoldFiles();
|
|
981
|
+
const written = [];
|
|
982
|
+
const skipped = [];
|
|
983
|
+
for (const file of files) {
|
|
984
|
+
const absolutePath = path9.join(options.cwd, file.path);
|
|
985
|
+
const exists = await pathExists(absolutePath);
|
|
986
|
+
if (exists && !options.force) {
|
|
987
|
+
skipped.push(file.path);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (!options.dryRun) {
|
|
991
|
+
await fs7.mkdir(path9.dirname(absolutePath), { recursive: true });
|
|
992
|
+
await fs7.writeFile(absolutePath, file.content);
|
|
993
|
+
}
|
|
994
|
+
written.push(file.path);
|
|
995
|
+
}
|
|
996
|
+
return { files, written, skipped };
|
|
997
|
+
}
|
|
998
|
+
function scaffoldFiles() {
|
|
999
|
+
return [
|
|
1000
|
+
{
|
|
1001
|
+
path: "agent/knowledge/README.md",
|
|
1002
|
+
content: knowledgeReadme()
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
path: "agent/knowledge/.eveknowledgeignore",
|
|
1006
|
+
content: `${defaultIgnorePatterns.join("\n")}
|
|
1007
|
+
`
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
path: "agent/tools/search_knowledge.ts",
|
|
1011
|
+
content: searchKnowledgeTool()
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
path: "agent/skills/answer-with-citations.md",
|
|
1015
|
+
content: citationSkill()
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
path: "eve-knowledge.config.json",
|
|
1019
|
+
content: configFile()
|
|
1020
|
+
}
|
|
1021
|
+
];
|
|
1022
|
+
}
|
|
1023
|
+
function knowledgeReadme() {
|
|
1024
|
+
return `# Agent Knowledge
|
|
1025
|
+
|
|
1026
|
+
Place reference docs for this Eve agent here. This folder is indexed by eve-knowledge; Eve core does not currently load agent/knowledge as a native slot.
|
|
1027
|
+
|
|
1028
|
+
Recommended folders:
|
|
1029
|
+
|
|
1030
|
+
- product/
|
|
1031
|
+
- runbooks/
|
|
1032
|
+
- decisions/
|
|
1033
|
+
- policies/
|
|
1034
|
+
|
|
1035
|
+
Do not store credentials, private customer data, or regulated records here unless your team has consent, retention, deletion, and access-control rules in place.
|
|
1036
|
+
`;
|
|
1037
|
+
}
|
|
1038
|
+
function searchKnowledgeTool() {
|
|
1039
|
+
return `import { defineTool } from "eve/tools";
|
|
1040
|
+
import { parseSearchKnowledgeInput, searchKnowledge, toModelOutput } from "eve-knowledge";
|
|
1041
|
+
|
|
1042
|
+
const inputSchema = {
|
|
1043
|
+
type: "object",
|
|
1044
|
+
properties: {
|
|
1045
|
+
query: {
|
|
1046
|
+
type: "string",
|
|
1047
|
+
minLength: 1,
|
|
1048
|
+
description: "The natural-language knowledge search query.",
|
|
1049
|
+
},
|
|
1050
|
+
topK: {
|
|
1051
|
+
type: "integer",
|
|
1052
|
+
minimum: 1,
|
|
1053
|
+
maximum: 10,
|
|
1054
|
+
description: "Maximum number of cited chunks to return.",
|
|
1055
|
+
},
|
|
1056
|
+
filters: {
|
|
1057
|
+
type: "object",
|
|
1058
|
+
additionalProperties: true,
|
|
1059
|
+
description: "Optional metadata filters such as audience, tenant, or product.",
|
|
1060
|
+
},
|
|
1061
|
+
},
|
|
1062
|
+
required: ["query"],
|
|
1063
|
+
additionalProperties: false,
|
|
1064
|
+
} as const;
|
|
1065
|
+
|
|
1066
|
+
export default defineTool({
|
|
1067
|
+
description: "Search the agent knowledge base and return cited results.",
|
|
1068
|
+
inputSchema,
|
|
1069
|
+
async execute(input) {
|
|
1070
|
+
return searchKnowledge(parseSearchKnowledgeInput(input));
|
|
1071
|
+
},
|
|
1072
|
+
toModelOutput(output) {
|
|
1073
|
+
return toModelOutput(output);
|
|
1074
|
+
},
|
|
1075
|
+
});
|
|
1076
|
+
`;
|
|
1077
|
+
}
|
|
1078
|
+
function citationSkill() {
|
|
1079
|
+
return `---
|
|
1080
|
+
description: Answer using retrieved eve-knowledge citations.
|
|
1081
|
+
---
|
|
1082
|
+
|
|
1083
|
+
# Answer With Citations
|
|
1084
|
+
|
|
1085
|
+
When answering from knowledge search results:
|
|
1086
|
+
|
|
1087
|
+
- Use only the retrieved evidence.
|
|
1088
|
+
- Cite repo-relative source paths and headings when available.
|
|
1089
|
+
- If the evidence is missing or weak, say you do not know.
|
|
1090
|
+
- Do not expose secrets, raw private data, or unrelated chunks.
|
|
1091
|
+
`;
|
|
1092
|
+
}
|
|
1093
|
+
function configFile() {
|
|
1094
|
+
return `{
|
|
1095
|
+
"knowledgeDir": "agent/knowledge",
|
|
1096
|
+
"storeDir": ".eve-knowledge",
|
|
1097
|
+
"redaction": {
|
|
1098
|
+
"mode": "warn"
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
`;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/index.ts
|
|
1105
|
+
var version = "0.1.0";
|
|
1106
|
+
export {
|
|
1107
|
+
LocalKnowledgeStore,
|
|
1108
|
+
checkKnowledge,
|
|
1109
|
+
chunkDocument,
|
|
1110
|
+
citationForChunk,
|
|
1111
|
+
countSources,
|
|
1112
|
+
createChunkId,
|
|
1113
|
+
createLocalKnowledgeStore,
|
|
1114
|
+
defineKnowledgeConfig,
|
|
1115
|
+
detectKnowledgeFormat,
|
|
1116
|
+
detectSecrets,
|
|
1117
|
+
indexKnowledge,
|
|
1118
|
+
listSourcePaths,
|
|
1119
|
+
loadKnowledgeConfig,
|
|
1120
|
+
loadKnowledgeDocument,
|
|
1121
|
+
matchesMetadataFilters,
|
|
1122
|
+
parseSearchKnowledgeInput,
|
|
1123
|
+
removeChunksBySource,
|
|
1124
|
+
replaceChunksBySource,
|
|
1125
|
+
resolveKnowledgeConfig,
|
|
1126
|
+
runKnowledgeEvals,
|
|
1127
|
+
scaffoldEveKnowledge,
|
|
1128
|
+
scaffoldFiles,
|
|
1129
|
+
scanKnowledgeFiles,
|
|
1130
|
+
searchKnowledge,
|
|
1131
|
+
searchKnowledgeInputSchema,
|
|
1132
|
+
toModelOutput,
|
|
1133
|
+
version
|
|
1134
|
+
};
|
|
1135
|
+
//# sourceMappingURL=index.js.map
|