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/cli.js
ADDED
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/indexer.ts
|
|
7
|
+
import { performance } from "perf_hooks";
|
|
8
|
+
|
|
9
|
+
// src/hash.ts
|
|
10
|
+
import { createHash } from "crypto";
|
|
11
|
+
function sha256(input) {
|
|
12
|
+
return createHash("sha256").update(input).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
function shortHash(input, length = 16) {
|
|
15
|
+
return sha256(input).slice(0, length);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/chunk.ts
|
|
19
|
+
function chunkDocument(document, config, indexedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
20
|
+
const chunks = [];
|
|
21
|
+
for (const section of document.sections) {
|
|
22
|
+
const parts = splitText(section.text, config.chunking.maxCharacters, config.chunking.overlapCharacters);
|
|
23
|
+
parts.forEach((text, partIndex) => {
|
|
24
|
+
const contentHash = shortHash(text, 32);
|
|
25
|
+
const ordinal = chunks.length;
|
|
26
|
+
chunks.push({
|
|
27
|
+
id: createChunkId(document.source.path, section.headingPath, section.ordinal, partIndex, contentHash),
|
|
28
|
+
source: document.source,
|
|
29
|
+
text,
|
|
30
|
+
headingPath: section.headingPath,
|
|
31
|
+
ordinal,
|
|
32
|
+
contentHash,
|
|
33
|
+
tokenCount: estimateTokenCount(text),
|
|
34
|
+
charCount: text.length,
|
|
35
|
+
indexedAt
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return chunks;
|
|
40
|
+
}
|
|
41
|
+
function createChunkId(sourcePath, headingPath, sectionOrdinal, partOrdinal, contentHash) {
|
|
42
|
+
return `chk_${shortHash(
|
|
43
|
+
[sourcePath, headingPath.join(" > "), sectionOrdinal, partOrdinal, contentHash].join("\n"),
|
|
44
|
+
24
|
|
45
|
+
)}`;
|
|
46
|
+
}
|
|
47
|
+
function splitText(text, maxCharacters, overlapCharacters) {
|
|
48
|
+
const normalized = text.trim();
|
|
49
|
+
if (!normalized) return [];
|
|
50
|
+
if (normalized.length <= maxCharacters) return [normalized];
|
|
51
|
+
const parts = [];
|
|
52
|
+
let start = 0;
|
|
53
|
+
while (start < normalized.length) {
|
|
54
|
+
const hardEnd = Math.min(start + maxCharacters, normalized.length);
|
|
55
|
+
const end = chooseBreak(normalized, start, hardEnd);
|
|
56
|
+
const part = normalized.slice(start, end).trim();
|
|
57
|
+
if (part) parts.push(part);
|
|
58
|
+
if (end >= normalized.length) break;
|
|
59
|
+
start = Math.max(end - overlapCharacters, start + 1);
|
|
60
|
+
}
|
|
61
|
+
return parts;
|
|
62
|
+
}
|
|
63
|
+
function chooseBreak(text, start, hardEnd) {
|
|
64
|
+
if (hardEnd >= text.length) return text.length;
|
|
65
|
+
const window = text.slice(start, hardEnd);
|
|
66
|
+
const paragraphBreak = window.lastIndexOf("\n\n");
|
|
67
|
+
if (paragraphBreak > Math.floor(window.length * 0.5)) {
|
|
68
|
+
return start + paragraphBreak;
|
|
69
|
+
}
|
|
70
|
+
const sentenceBreak = Math.max(window.lastIndexOf(". "), window.lastIndexOf("? "), window.lastIndexOf("! "));
|
|
71
|
+
if (sentenceBreak > Math.floor(window.length * 0.5)) {
|
|
72
|
+
return start + sentenceBreak + 1;
|
|
73
|
+
}
|
|
74
|
+
const wordBreak = window.lastIndexOf(" ");
|
|
75
|
+
if (wordBreak > Math.floor(window.length * 0.5)) {
|
|
76
|
+
return start + wordBreak;
|
|
77
|
+
}
|
|
78
|
+
return hardEnd;
|
|
79
|
+
}
|
|
80
|
+
function estimateTokenCount(text) {
|
|
81
|
+
return Math.ceil(text.length / 4);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/config.ts
|
|
85
|
+
import path from "path";
|
|
86
|
+
var defaultIncludePatterns = ["**/*.{md,mdx,txt,json,yaml,yml}"];
|
|
87
|
+
var defaultIgnorePatterns = [
|
|
88
|
+
".git/**",
|
|
89
|
+
"node_modules/**",
|
|
90
|
+
"dist/**",
|
|
91
|
+
"build/**",
|
|
92
|
+
".next/**",
|
|
93
|
+
".nuxt/**",
|
|
94
|
+
".output/**",
|
|
95
|
+
".vercel/**",
|
|
96
|
+
".turbo/**",
|
|
97
|
+
".cache/**",
|
|
98
|
+
".eve-knowledge/**",
|
|
99
|
+
"**/.env",
|
|
100
|
+
"**/.env.*",
|
|
101
|
+
"**/*.pem",
|
|
102
|
+
"**/*.key",
|
|
103
|
+
"**/*secret*",
|
|
104
|
+
"**/*credential*"
|
|
105
|
+
];
|
|
106
|
+
var defaultMaxFileBytes = 256 * 1024;
|
|
107
|
+
var defaultChunking = {
|
|
108
|
+
maxCharacters: 1600,
|
|
109
|
+
overlapCharacters: 160
|
|
110
|
+
};
|
|
111
|
+
function resolveKnowledgeConfig(config = {}, cwd = process.cwd()) {
|
|
112
|
+
const rootDir = path.resolve(cwd, config.rootDir ?? ".");
|
|
113
|
+
const agentDir = path.resolve(rootDir, config.agentDir ?? "agent");
|
|
114
|
+
const knowledgeDir = path.resolve(rootDir, config.knowledgeDir ?? "agent/knowledge");
|
|
115
|
+
const storeDir = path.resolve(rootDir, config.storeDir ?? ".eve-knowledge");
|
|
116
|
+
const chunking = {
|
|
117
|
+
...defaultChunking,
|
|
118
|
+
...config.chunking
|
|
119
|
+
};
|
|
120
|
+
if (chunking.maxCharacters <= 0) {
|
|
121
|
+
throw new Error("chunking.maxCharacters must be greater than 0");
|
|
122
|
+
}
|
|
123
|
+
if (chunking.overlapCharacters < 0) {
|
|
124
|
+
throw new Error("chunking.overlapCharacters must be zero or greater");
|
|
125
|
+
}
|
|
126
|
+
if (chunking.overlapCharacters >= chunking.maxCharacters) {
|
|
127
|
+
throw new Error("chunking.overlapCharacters must be smaller than chunking.maxCharacters");
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
rootDir,
|
|
131
|
+
agentDir,
|
|
132
|
+
knowledgeDir,
|
|
133
|
+
storeDir,
|
|
134
|
+
include: config.include ?? defaultIncludePatterns,
|
|
135
|
+
ignore: [...defaultIgnorePatterns, ...config.ignore ?? []],
|
|
136
|
+
maxFileBytes: config.maxFileBytes ?? defaultMaxFileBytes,
|
|
137
|
+
chunking,
|
|
138
|
+
redaction: {
|
|
139
|
+
mode: config.redaction?.mode ?? "warn"
|
|
140
|
+
},
|
|
141
|
+
memory: config.memory ?? { enabled: false }
|
|
142
|
+
};
|
|
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/search.ts
|
|
822
|
+
async function searchKnowledge(input, options = {}) {
|
|
823
|
+
const config = resolveKnowledgeConfig(options.config, options.cwd);
|
|
824
|
+
const store = options.store ?? createLocalKnowledgeStore({ storeDir: config.storeDir });
|
|
825
|
+
const topK = Math.min(Math.max(input.topK ?? options.maxResults ?? 5, 1), 20);
|
|
826
|
+
const results = await store.search({
|
|
827
|
+
query: input.query,
|
|
828
|
+
topK,
|
|
829
|
+
...input.filters ? { filters: input.filters } : {}
|
|
830
|
+
});
|
|
831
|
+
if (results.length === 0) {
|
|
832
|
+
return {
|
|
833
|
+
status: "no_results",
|
|
834
|
+
query: input.query,
|
|
835
|
+
message: "No relevant knowledge results were found. Do not fabricate an answer; ask for source material or say you do not know."
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
return {
|
|
839
|
+
status: "results",
|
|
840
|
+
query: input.query,
|
|
841
|
+
results
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/scaffold.ts
|
|
846
|
+
import fs7 from "fs/promises";
|
|
847
|
+
import path9 from "path";
|
|
848
|
+
|
|
849
|
+
// src/project.ts
|
|
850
|
+
import fs6 from "fs/promises";
|
|
851
|
+
import path8 from "path";
|
|
852
|
+
async function detectEveProject(cwd) {
|
|
853
|
+
const signals = [];
|
|
854
|
+
const agentDir = path8.join(cwd, "agent");
|
|
855
|
+
if (await pathExists(agentDir)) {
|
|
856
|
+
signals.push("agent/");
|
|
857
|
+
}
|
|
858
|
+
for (const filePath of ["agent/agent.ts", "agent/instructions.md", "agent/tools"]) {
|
|
859
|
+
if (await pathExists(path8.join(cwd, filePath))) {
|
|
860
|
+
signals.push(filePath);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (await packageHasEveDependency(path8.join(cwd, "package.json"))) {
|
|
864
|
+
signals.push("package.json:eve");
|
|
865
|
+
}
|
|
866
|
+
return {
|
|
867
|
+
isEveProject: signals.some((signal) => signal !== "agent/"),
|
|
868
|
+
signals
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
async function packageHasEveDependency(filePath) {
|
|
872
|
+
try {
|
|
873
|
+
const pkg = JSON.parse(await fs6.readFile(filePath, "utf8"));
|
|
874
|
+
return Boolean(pkg.dependencies?.eve ?? pkg.devDependencies?.eve);
|
|
875
|
+
} catch {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/scaffold.ts
|
|
881
|
+
async function scaffoldEveKnowledge(options) {
|
|
882
|
+
const detection = await detectEveProject(options.cwd);
|
|
883
|
+
if (!detection.isEveProject && !options.allowNonEve) {
|
|
884
|
+
throw new Error(
|
|
885
|
+
"No Eve project detected. Run this inside an Eve app or pass --allow-non-eve to scaffold anyway."
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
const files = scaffoldFiles();
|
|
889
|
+
const written = [];
|
|
890
|
+
const skipped = [];
|
|
891
|
+
for (const file of files) {
|
|
892
|
+
const absolutePath = path9.join(options.cwd, file.path);
|
|
893
|
+
const exists = await pathExists(absolutePath);
|
|
894
|
+
if (exists && !options.force) {
|
|
895
|
+
skipped.push(file.path);
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
if (!options.dryRun) {
|
|
899
|
+
await fs7.mkdir(path9.dirname(absolutePath), { recursive: true });
|
|
900
|
+
await fs7.writeFile(absolutePath, file.content);
|
|
901
|
+
}
|
|
902
|
+
written.push(file.path);
|
|
903
|
+
}
|
|
904
|
+
return { files, written, skipped };
|
|
905
|
+
}
|
|
906
|
+
function scaffoldFiles() {
|
|
907
|
+
return [
|
|
908
|
+
{
|
|
909
|
+
path: "agent/knowledge/README.md",
|
|
910
|
+
content: knowledgeReadme()
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
path: "agent/knowledge/.eveknowledgeignore",
|
|
914
|
+
content: `${defaultIgnorePatterns.join("\n")}
|
|
915
|
+
`
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
path: "agent/tools/search_knowledge.ts",
|
|
919
|
+
content: searchKnowledgeTool()
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
path: "agent/skills/answer-with-citations.md",
|
|
923
|
+
content: citationSkill()
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
path: "eve-knowledge.config.json",
|
|
927
|
+
content: configFile()
|
|
928
|
+
}
|
|
929
|
+
];
|
|
930
|
+
}
|
|
931
|
+
function knowledgeReadme() {
|
|
932
|
+
return `# Agent Knowledge
|
|
933
|
+
|
|
934
|
+
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.
|
|
935
|
+
|
|
936
|
+
Recommended folders:
|
|
937
|
+
|
|
938
|
+
- product/
|
|
939
|
+
- runbooks/
|
|
940
|
+
- decisions/
|
|
941
|
+
- policies/
|
|
942
|
+
|
|
943
|
+
Do not store credentials, private customer data, or regulated records here unless your team has consent, retention, deletion, and access-control rules in place.
|
|
944
|
+
`;
|
|
945
|
+
}
|
|
946
|
+
function searchKnowledgeTool() {
|
|
947
|
+
return `import { defineTool } from "eve/tools";
|
|
948
|
+
import { parseSearchKnowledgeInput, searchKnowledge, toModelOutput } from "eve-knowledge";
|
|
949
|
+
|
|
950
|
+
const inputSchema = {
|
|
951
|
+
type: "object",
|
|
952
|
+
properties: {
|
|
953
|
+
query: {
|
|
954
|
+
type: "string",
|
|
955
|
+
minLength: 1,
|
|
956
|
+
description: "The natural-language knowledge search query.",
|
|
957
|
+
},
|
|
958
|
+
topK: {
|
|
959
|
+
type: "integer",
|
|
960
|
+
minimum: 1,
|
|
961
|
+
maximum: 10,
|
|
962
|
+
description: "Maximum number of cited chunks to return.",
|
|
963
|
+
},
|
|
964
|
+
filters: {
|
|
965
|
+
type: "object",
|
|
966
|
+
additionalProperties: true,
|
|
967
|
+
description: "Optional metadata filters such as audience, tenant, or product.",
|
|
968
|
+
},
|
|
969
|
+
},
|
|
970
|
+
required: ["query"],
|
|
971
|
+
additionalProperties: false,
|
|
972
|
+
} as const;
|
|
973
|
+
|
|
974
|
+
export default defineTool({
|
|
975
|
+
description: "Search the agent knowledge base and return cited results.",
|
|
976
|
+
inputSchema,
|
|
977
|
+
async execute(input) {
|
|
978
|
+
return searchKnowledge(parseSearchKnowledgeInput(input));
|
|
979
|
+
},
|
|
980
|
+
toModelOutput(output) {
|
|
981
|
+
return toModelOutput(output);
|
|
982
|
+
},
|
|
983
|
+
});
|
|
984
|
+
`;
|
|
985
|
+
}
|
|
986
|
+
function citationSkill() {
|
|
987
|
+
return `---
|
|
988
|
+
description: Answer using retrieved eve-knowledge citations.
|
|
989
|
+
---
|
|
990
|
+
|
|
991
|
+
# Answer With Citations
|
|
992
|
+
|
|
993
|
+
When answering from knowledge search results:
|
|
994
|
+
|
|
995
|
+
- Use only the retrieved evidence.
|
|
996
|
+
- Cite repo-relative source paths and headings when available.
|
|
997
|
+
- If the evidence is missing or weak, say you do not know.
|
|
998
|
+
- Do not expose secrets, raw private data, or unrelated chunks.
|
|
999
|
+
`;
|
|
1000
|
+
}
|
|
1001
|
+
function configFile() {
|
|
1002
|
+
return `{
|
|
1003
|
+
"knowledgeDir": "agent/knowledge",
|
|
1004
|
+
"storeDir": ".eve-knowledge",
|
|
1005
|
+
"redaction": {
|
|
1006
|
+
"mode": "warn"
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
`;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// src/index.ts
|
|
1013
|
+
var version = "0.1.0";
|
|
1014
|
+
|
|
1015
|
+
// src/cli.ts
|
|
1016
|
+
function createCli() {
|
|
1017
|
+
const program = new Command();
|
|
1018
|
+
program.name("eve-knowledge").description("Add a cited agent/knowledge folder to Eve agents.").version(version);
|
|
1019
|
+
program.command("init").description("Create the agent/knowledge convention, Eve tool, citation skill, and config.").option("--dry-run", "Print planned files without writing them.").option("--force", "Overwrite existing generated files.").option("--allow-non-eve", "Scaffold even if no Eve project is detected.").action(async (options) => {
|
|
1020
|
+
const result = await scaffoldEveKnowledge({
|
|
1021
|
+
cwd: process.cwd(),
|
|
1022
|
+
...options.dryRun ? { dryRun: true } : {},
|
|
1023
|
+
...options.force ? { force: true } : {},
|
|
1024
|
+
...options.allowNonEve ? { allowNonEve: true } : {}
|
|
1025
|
+
});
|
|
1026
|
+
printList(options.dryRun ? "Would write" : "Written", result.written);
|
|
1027
|
+
printList("Skipped existing files", result.skipped);
|
|
1028
|
+
if (result.skipped.length > 0 && !options.force) {
|
|
1029
|
+
console.log("Use --force to overwrite skipped files.");
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
program.command("index").description("Index files from agent/knowledge into the configured local store.").option("--trusted-config", "Allow executable eve-knowledge.config.ts/js/mjs. Use only with trusted code.").action(async (options) => {
|
|
1033
|
+
const config = await loadKnowledgeConfig({ trustedConfig: Boolean(options.trustedConfig) });
|
|
1034
|
+
const summary = await indexKnowledge({ config });
|
|
1035
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
1036
|
+
if (summary.errors.length > 0) {
|
|
1037
|
+
process.exitCode = 1;
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
program.command("search").description("Search the local knowledge store.").argument("<query>", "Knowledge search query.").option("--top-k <number>", "Maximum number of results.", parseInteger).option("--trusted-config", "Allow executable eve-knowledge.config.ts/js/mjs. Use only with trusted code.").action(async (query, options) => {
|
|
1041
|
+
const config = await loadKnowledgeConfig({ trustedConfig: Boolean(options.trustedConfig) });
|
|
1042
|
+
const response = await searchKnowledge(
|
|
1043
|
+
{
|
|
1044
|
+
query,
|
|
1045
|
+
...options.topK !== void 0 ? { topK: options.topK } : {}
|
|
1046
|
+
},
|
|
1047
|
+
{ config }
|
|
1048
|
+
);
|
|
1049
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1050
|
+
});
|
|
1051
|
+
program.command("check").description("Validate that knowledge can be indexed safely for CI.").option("--production", "Fail if the configured store is not durable.").option("--trusted-config", "Allow executable eve-knowledge.config.ts/js/mjs. Use only with trusted code.").action(async (options) => {
|
|
1052
|
+
const loadedConfig = await loadKnowledgeConfig({ trustedConfig: Boolean(options.trustedConfig) });
|
|
1053
|
+
const result = await checkKnowledge({
|
|
1054
|
+
config: {
|
|
1055
|
+
...loadedConfig
|
|
1056
|
+
},
|
|
1057
|
+
...options.production ? { production: true } : {}
|
|
1058
|
+
});
|
|
1059
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1060
|
+
if (!result.ok) {
|
|
1061
|
+
process.exitCode = 1;
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
return program;
|
|
1065
|
+
}
|
|
1066
|
+
async function runCli(argv = process.argv) {
|
|
1067
|
+
await createCli().parseAsync(argv);
|
|
1068
|
+
}
|
|
1069
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
1070
|
+
await runCli();
|
|
1071
|
+
}
|
|
1072
|
+
function printList(label, values) {
|
|
1073
|
+
if (values.length === 0) return;
|
|
1074
|
+
console.log(`${label}:`);
|
|
1075
|
+
for (const value of values) {
|
|
1076
|
+
console.log(` - ${value}`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function parseInteger(value) {
|
|
1080
|
+
const parsed = Number.parseInt(value, 10);
|
|
1081
|
+
if (!Number.isInteger(parsed)) {
|
|
1082
|
+
throw new Error(`Expected an integer, received ${value}`);
|
|
1083
|
+
}
|
|
1084
|
+
return parsed;
|
|
1085
|
+
}
|
|
1086
|
+
export {
|
|
1087
|
+
createCli,
|
|
1088
|
+
runCli
|
|
1089
|
+
};
|
|
1090
|
+
//# sourceMappingURL=cli.js.map
|