botholomew 0.16.4 → 0.18.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/README.md +46 -41
- package/package.json +4 -9
- package/src/chat/agent.ts +37 -40
- package/src/chat/session.ts +10 -10
- package/src/cli.ts +0 -2
- package/src/commands/capabilities.ts +35 -33
- package/src/commands/context.ts +133 -221
- package/src/commands/init.ts +22 -1
- package/src/commands/mcpx.ts +21 -8
- package/src/commands/nuke.ts +52 -15
- package/src/commands/prepare.ts +16 -13
- package/src/config/loader.ts +1 -8
- package/src/config/schemas.ts +6 -0
- package/src/constants.ts +16 -32
- package/src/init/index.ts +52 -27
- package/src/mcpx/client.ts +21 -5
- package/src/mem/client.ts +33 -0
- package/src/{context → prompts}/capabilities.ts +11 -7
- package/src/schedules/store.ts +1 -1
- package/src/tasks/store.ts +1 -1
- package/src/threads/store.ts +1 -1
- package/src/tools/capabilities/refresh.ts +1 -1
- package/src/tools/membot/adapter.ts +111 -0
- package/src/tools/membot/copy.ts +59 -0
- package/src/tools/membot/count_lines.ts +53 -0
- package/src/tools/membot/edit.ts +72 -0
- package/src/tools/membot/exists.ts +54 -0
- package/src/tools/membot/index.ts +26 -0
- package/src/tools/{context → membot}/pipe.ts +34 -32
- package/src/tools/registry.ts +6 -37
- package/src/tools/tool.ts +6 -8
- package/src/tui/App.tsx +3 -4
- package/src/tui/components/ContextPanel.tsx +109 -226
- package/src/tui/components/HelpPanel.tsx +2 -2
- package/src/tui/components/StatusBar.tsx +0 -6
- package/src/tui/components/ThreadPanel.tsx +8 -7
- package/src/tui/wrapDetail.ts +11 -0
- package/src/worker/heartbeat.ts +0 -20
- package/src/worker/index.ts +13 -13
- package/src/worker/llm.ts +7 -9
- package/src/worker/prompt.ts +25 -13
- package/src/worker/spawn.ts +1 -1
- package/src/worker/tick.ts +10 -9
- package/src/commands/db.ts +0 -119
- package/src/commands/with-db.ts +0 -22
- package/src/context/chunker.ts +0 -275
- package/src/context/embedder-impl.ts +0 -100
- package/src/context/embedder.ts +0 -9
- package/src/context/fetcher-errors.ts +0 -8
- package/src/context/fetcher.ts +0 -515
- package/src/context/locks.ts +0 -146
- package/src/context/markdown-converter.ts +0 -186
- package/src/context/reindex.ts +0 -198
- package/src/context/store.ts +0 -841
- package/src/context/url-utils.ts +0 -25
- package/src/db/connection.ts +0 -255
- package/src/db/doctor.ts +0 -235
- package/src/db/embeddings.ts +0 -317
- package/src/db/query.ts +0 -56
- package/src/db/schema.ts +0 -93
- package/src/db/sql/1-core_tables.sql +0 -53
- package/src/db/sql/10-dedupe_context_items.sql +0 -26
- package/src/db/sql/11-rebuild_hnsw.sql +0 -8
- package/src/db/sql/12-workers.sql +0 -66
- package/src/db/sql/13-drive-paths.sql +0 -47
- package/src/db/sql/14-drop_hnsw_index.sql +0 -8
- package/src/db/sql/15-fts_index.sql +0 -8
- package/src/db/sql/16-source_url.sql +0 -7
- package/src/db/sql/17-worker_log_path.sql +0 -3
- package/src/db/sql/18-reset_embeddings_for_local.sql +0 -39
- package/src/db/sql/19-disk_backed_index.sql +0 -36
- package/src/db/sql/2-logging_tables.sql +0 -24
- package/src/db/sql/20-drop_db_tables_for_files.sql +0 -19
- package/src/db/sql/3-daemon_state.sql +0 -5
- package/src/db/sql/4-unique_context_path.sql +0 -1
- package/src/db/sql/5-reset_embeddings_for_openai.sql +0 -1
- package/src/db/sql/6-vss_index.sql +0 -7
- package/src/db/sql/7-drop_embeddings_fk.sql +0 -23
- package/src/db/sql/8-task_output.sql +0 -1
- package/src/db/sql/9-source-type.sql +0 -1
- package/src/tools/context/read-large-result.ts +0 -33
- package/src/tools/dir/create.ts +0 -47
- package/src/tools/dir/size.ts +0 -77
- package/src/tools/dir/tree.ts +0 -124
- package/src/tools/file/copy.ts +0 -73
- package/src/tools/file/count-lines.ts +0 -54
- package/src/tools/file/delete.ts +0 -83
- package/src/tools/file/edit.ts +0 -76
- package/src/tools/file/exists.ts +0 -33
- package/src/tools/file/info.ts +0 -66
- package/src/tools/file/move.ts +0 -66
- package/src/tools/file/read.ts +0 -67
- package/src/tools/file/write.ts +0 -58
- package/src/tools/search/fuse.ts +0 -96
- package/src/tools/search/index.ts +0 -127
- package/src/tools/search/regexp.ts +0 -82
- package/src/tools/search/semantic.ts +0 -167
- /package/src/{db → utils}/uuid.ts +0 -0
package/src/context/chunker.ts
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
-
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
3
|
-
import { logger } from "../utils/logger.ts";
|
|
4
|
-
|
|
5
|
-
export interface Chunk {
|
|
6
|
-
index: number;
|
|
7
|
-
content: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const SHORT_CONTENT_THRESHOLD = 200;
|
|
11
|
-
const LLM_TIMEOUT_MS = 10_000;
|
|
12
|
-
const DEFAULT_OVERLAP_LINES = 2;
|
|
13
|
-
// OpenAI's embedding endpoint caps inputs at 8192 tokens. The cl100k_base
|
|
14
|
-
// tokenizer averages ~4 chars/token on plain English but can drop to ~2
|
|
15
|
-
// chars/token on dense/code/non-ASCII content. We cap at 15k chars so even
|
|
16
|
-
// at the worst-case ~2.5 chars/token (~6k tokens) we stay well under the
|
|
17
|
-
// 8192-token limit, leaving headroom for the title/description prefix
|
|
18
|
-
// prepended at embed time.
|
|
19
|
-
const MAX_CHUNK_CHARS = 15_000;
|
|
20
|
-
// Target size for deterministic fallback chunks. Smaller than MAX_CHUNK_CHARS
|
|
21
|
-
// so a large doc produces multiple chunks of reasonable granularity when the
|
|
22
|
-
// LLM chunker fails.
|
|
23
|
-
const FALLBACK_TARGET_CHARS = 4_000;
|
|
24
|
-
|
|
25
|
-
const CHUNKER_TOOL_NAME = "return_chunks";
|
|
26
|
-
const CHUNKER_TOOL = {
|
|
27
|
-
name: CHUNKER_TOOL_NAME,
|
|
28
|
-
description:
|
|
29
|
-
"Return the chunk boundaries for this document. Each chunk should be a coherent semantic section.",
|
|
30
|
-
input_schema: {
|
|
31
|
-
type: "object" as const,
|
|
32
|
-
properties: {
|
|
33
|
-
chunks: {
|
|
34
|
-
type: "array",
|
|
35
|
-
description: "Array of chunk boundaries (1-based, inclusive)",
|
|
36
|
-
items: {
|
|
37
|
-
type: "object",
|
|
38
|
-
properties: {
|
|
39
|
-
start_line: {
|
|
40
|
-
type: "number",
|
|
41
|
-
description: "1-based start line (inclusive)",
|
|
42
|
-
},
|
|
43
|
-
end_line: {
|
|
44
|
-
type: "number",
|
|
45
|
-
description: "1-based end line (inclusive)",
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
required: ["start_line", "end_line"],
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
required: ["chunks"],
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Split text into pieces no larger than `maxChars`, preferring paragraph,
|
|
58
|
-
* line, and finally hard-character boundaries.
|
|
59
|
-
*/
|
|
60
|
-
function splitText(text: string, maxChars: number): string[] {
|
|
61
|
-
if (text.length <= maxChars) return [text];
|
|
62
|
-
|
|
63
|
-
// Try paragraph splits first.
|
|
64
|
-
const paragraphs = text.split(/\n\n+/);
|
|
65
|
-
if (paragraphs.length > 1) {
|
|
66
|
-
const out: string[] = [];
|
|
67
|
-
let buf = "";
|
|
68
|
-
for (const p of paragraphs) {
|
|
69
|
-
const candidate = buf ? `${buf}\n\n${p}` : p;
|
|
70
|
-
if (candidate.length <= maxChars) {
|
|
71
|
-
buf = candidate;
|
|
72
|
-
} else {
|
|
73
|
-
if (buf) out.push(buf);
|
|
74
|
-
if (p.length <= maxChars) {
|
|
75
|
-
buf = p;
|
|
76
|
-
} else {
|
|
77
|
-
out.push(...splitText(p, maxChars));
|
|
78
|
-
buf = "";
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
if (buf) out.push(buf);
|
|
83
|
-
return out;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Fall back to line splits.
|
|
87
|
-
const lines = text.split("\n");
|
|
88
|
-
if (lines.length > 1) {
|
|
89
|
-
const out: string[] = [];
|
|
90
|
-
let buf = "";
|
|
91
|
-
for (const line of lines) {
|
|
92
|
-
const candidate = buf ? `${buf}\n${line}` : line;
|
|
93
|
-
if (candidate.length <= maxChars) {
|
|
94
|
-
buf = candidate;
|
|
95
|
-
} else {
|
|
96
|
-
if (buf) out.push(buf);
|
|
97
|
-
if (line.length <= maxChars) {
|
|
98
|
-
buf = line;
|
|
99
|
-
} else {
|
|
100
|
-
// Single line longer than maxChars — slice it.
|
|
101
|
-
for (let i = 0; i < line.length; i += maxChars) {
|
|
102
|
-
out.push(line.slice(i, i + maxChars));
|
|
103
|
-
}
|
|
104
|
-
buf = "";
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (buf) out.push(buf);
|
|
109
|
-
return out;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Last resort: hard slice.
|
|
113
|
-
const out: string[] = [];
|
|
114
|
-
for (let i = 0; i < text.length; i += maxChars) {
|
|
115
|
-
out.push(text.slice(i, i + maxChars));
|
|
116
|
-
}
|
|
117
|
-
return out;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Re-chunk any chunks larger than `maxChars`, preserving order and reindexing.
|
|
122
|
-
*/
|
|
123
|
-
export function enforceMaxChunkSize(
|
|
124
|
-
chunks: Chunk[],
|
|
125
|
-
maxChars = MAX_CHUNK_CHARS,
|
|
126
|
-
): Chunk[] {
|
|
127
|
-
const out: Chunk[] = [];
|
|
128
|
-
for (const c of chunks) {
|
|
129
|
-
if (c.content.length <= maxChars) {
|
|
130
|
-
out.push({ index: out.length, content: c.content });
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
for (const piece of splitText(c.content, maxChars)) {
|
|
134
|
-
out.push({ index: out.length, content: piece });
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return out;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Add overlapping lines from the end of each chunk to the start of the next.
|
|
142
|
-
* Improves retrieval when concepts span chunk boundaries.
|
|
143
|
-
*/
|
|
144
|
-
export function addOverlapToChunks(
|
|
145
|
-
chunks: Chunk[],
|
|
146
|
-
overlapLines = DEFAULT_OVERLAP_LINES,
|
|
147
|
-
): Chunk[] {
|
|
148
|
-
if (chunks.length <= 1 || overlapLines <= 0) return chunks;
|
|
149
|
-
|
|
150
|
-
return chunks.map((c, i) => {
|
|
151
|
-
if (i === 0) return { ...c };
|
|
152
|
-
const prevChunk = chunks[i - 1];
|
|
153
|
-
if (!prevChunk) return { ...c };
|
|
154
|
-
const prevLines = prevChunk.content.split("\n");
|
|
155
|
-
const overlap = prevLines.slice(-overlapLines).join("\n");
|
|
156
|
-
return { ...c, content: `${overlap}\n${c.content}` };
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export type LLMChunkerFn = (
|
|
161
|
-
content: string,
|
|
162
|
-
mimeType: string,
|
|
163
|
-
config: Required<BotholomewConfig>,
|
|
164
|
-
) => Promise<Chunk[]>;
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Deterministic fallback that splits content on paragraph / line /
|
|
168
|
-
* hard-char boundaries. Used when the LLM chunker errors or times out.
|
|
169
|
-
*/
|
|
170
|
-
export function chunkByTextSplit(
|
|
171
|
-
content: string,
|
|
172
|
-
targetChars = FALLBACK_TARGET_CHARS,
|
|
173
|
-
): Chunk[] {
|
|
174
|
-
return splitText(content, targetChars).map((c, i) => ({
|
|
175
|
-
index: i,
|
|
176
|
-
content: c,
|
|
177
|
-
}));
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* LLM-driven chunker that asks Claude to identify semantic boundaries.
|
|
182
|
-
* Uses structured outputs via tool_use with forced tool_choice.
|
|
183
|
-
*/
|
|
184
|
-
export async function chunkWithLLM(
|
|
185
|
-
content: string,
|
|
186
|
-
mimeType: string,
|
|
187
|
-
config: Required<BotholomewConfig>,
|
|
188
|
-
): Promise<Chunk[]> {
|
|
189
|
-
const client = new Anthropic({ apiKey: config.anthropic_api_key });
|
|
190
|
-
const lines = content.split("\n");
|
|
191
|
-
|
|
192
|
-
const response = await Promise.race([
|
|
193
|
-
client.messages.create({
|
|
194
|
-
model: config.chunker_model,
|
|
195
|
-
max_tokens: 2048,
|
|
196
|
-
tools: [CHUNKER_TOOL],
|
|
197
|
-
tool_choice: { type: "tool", name: CHUNKER_TOOL_NAME },
|
|
198
|
-
messages: [
|
|
199
|
-
{
|
|
200
|
-
role: "user",
|
|
201
|
-
content: `You are a document chunker. Given the following ${mimeType} document with ${lines.length} lines, identify semantic chunk boundaries. Each chunk should be a coherent section (100-500 lines preferred). Cover all lines with no gaps.
|
|
202
|
-
|
|
203
|
-
Document:
|
|
204
|
-
${content}`,
|
|
205
|
-
},
|
|
206
|
-
],
|
|
207
|
-
}),
|
|
208
|
-
new Promise<never>((_, reject) =>
|
|
209
|
-
setTimeout(
|
|
210
|
-
() => reject(new Error("LLM chunker timeout")),
|
|
211
|
-
LLM_TIMEOUT_MS,
|
|
212
|
-
),
|
|
213
|
-
),
|
|
214
|
-
]);
|
|
215
|
-
|
|
216
|
-
// Extract the tool_use block
|
|
217
|
-
const toolBlock = response.content.find((b) => b.type === "tool_use");
|
|
218
|
-
if (!toolBlock || toolBlock.type !== "tool_use") {
|
|
219
|
-
throw new Error("LLM chunker returned no tool_use block");
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const input = toolBlock.input as {
|
|
223
|
-
chunks: Array<{ start_line: number; end_line: number }>;
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
if (!Array.isArray(input.chunks) || input.chunks.length === 0) {
|
|
227
|
-
throw new Error("LLM chunker returned empty boundaries");
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return input.chunks.map((b, i) => ({
|
|
231
|
-
index: i,
|
|
232
|
-
content: lines.slice(b.start_line - 1, b.end_line).join("\n"),
|
|
233
|
-
}));
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Chunk content using the LLM chunker, with a deterministic fallback
|
|
238
|
-
* when the LLM call fails (timeout, empty boundaries, API error, …).
|
|
239
|
-
* Short content (<200 chars) is returned as a single chunk.
|
|
240
|
-
*/
|
|
241
|
-
export async function chunk(
|
|
242
|
-
content: string,
|
|
243
|
-
mimeType: string,
|
|
244
|
-
config: Required<BotholomewConfig>,
|
|
245
|
-
llmChunker: LLMChunkerFn = chunkWithLLM,
|
|
246
|
-
): Promise<Chunk[]> {
|
|
247
|
-
if (content.length < SHORT_CONTENT_THRESHOLD) {
|
|
248
|
-
return [{ index: 0, content }];
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (!config.anthropic_api_key) {
|
|
252
|
-
throw new Error(
|
|
253
|
-
"Anthropic API key is required for chunking. Set anthropic_api_key in config.",
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
let chunks: Chunk[];
|
|
258
|
-
try {
|
|
259
|
-
chunks = await llmChunker(content, mimeType, config);
|
|
260
|
-
} catch (err) {
|
|
261
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
262
|
-
logger.warn(
|
|
263
|
-
`chunker: LLM chunking failed (${msg}); falling back to deterministic text split`,
|
|
264
|
-
);
|
|
265
|
-
chunks = chunkByTextSplit(content);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Enforce a hard size cap before AND after overlap. The first pass handles
|
|
269
|
-
// oversize chunks from the LLM (common for docs with very long lines); the
|
|
270
|
-
// second pass handles the rare case where added overlap pushes a near-limit
|
|
271
|
-
// chunk over.
|
|
272
|
-
const sized = enforceMaxChunkSize(chunks);
|
|
273
|
-
const withOverlap = addOverlapToChunks(sized);
|
|
274
|
-
return enforceMaxChunkSize(withOverlap);
|
|
275
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
env,
|
|
5
|
-
type FeatureExtractionPipeline,
|
|
6
|
-
pipeline,
|
|
7
|
-
} from "@huggingface/transformers";
|
|
8
|
-
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
9
|
-
import { logger } from "../utils/logger.ts";
|
|
10
|
-
|
|
11
|
-
// We patch @huggingface/transformers to use onnxruntime-web (WASM) instead of
|
|
12
|
-
// onnxruntime-node (which segfaults under Bun — oven-sh/bun#26081). By default
|
|
13
|
-
// transformers.js then points the WASM loader at jsDelivr; pin it to the
|
|
14
|
-
// onnxruntime-web copy already on disk so the chat path stays offline-capable.
|
|
15
|
-
const ortWasm = env.backends.onnx?.wasm;
|
|
16
|
-
if (ortWasm) {
|
|
17
|
-
ortWasm.wasmPaths = {
|
|
18
|
-
mjs: import.meta.resolve(
|
|
19
|
-
"onnxruntime-web/ort-wasm-simd-threaded.asyncify.mjs",
|
|
20
|
-
),
|
|
21
|
-
wasm: import.meta.resolve(
|
|
22
|
-
"onnxruntime-web/ort-wasm-simd-threaded.asyncify.wasm",
|
|
23
|
-
),
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
type EmbedFn = (
|
|
28
|
-
texts: string[],
|
|
29
|
-
config: Required<BotholomewConfig>,
|
|
30
|
-
) => Promise<number[][]>;
|
|
31
|
-
|
|
32
|
-
// Singleton pipeline keyed by model name. Loading the model is expensive
|
|
33
|
-
// (downloads weights on first run, then ~hundreds of ms to instantiate the
|
|
34
|
-
// ONNX runtime), so we hold one per model for the life of the process.
|
|
35
|
-
const pipelinePromises = new Map<string, Promise<FeatureExtractionPipeline>>();
|
|
36
|
-
|
|
37
|
-
export function setEmbeddingCacheDir(dir: string): void {
|
|
38
|
-
// Trailing separator matters: transformers.js builds paths as `${cacheDir}${rel}` (no separator).
|
|
39
|
-
env.cacheDir = dir.endsWith("/") ? dir : `${dir}/`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function isModelCached(model: string): boolean {
|
|
43
|
-
if (!env.cacheDir) return false;
|
|
44
|
-
return existsSync(join(env.cacheDir, model));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function getPipeline(model: string): Promise<FeatureExtractionPipeline> {
|
|
48
|
-
let p = pipelinePromises.get(model);
|
|
49
|
-
if (!p) {
|
|
50
|
-
if (isModelCached(model)) {
|
|
51
|
-
logger.debug(`Loading embedding model ${model}`);
|
|
52
|
-
} else {
|
|
53
|
-
logger.info(
|
|
54
|
-
`Loading embedding model ${model} (first run, downloading weights)`,
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
p = pipeline("feature-extraction", model);
|
|
58
|
-
pipelinePromises.set(model, p);
|
|
59
|
-
}
|
|
60
|
-
return p;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Embed multiple texts using a local @huggingface/transformers feature-extraction
|
|
65
|
-
* pipeline. Returns an array of L2-normalized float vectors with the model's
|
|
66
|
-
* native dimension (must match `config.embedding_dimension`).
|
|
67
|
-
*/
|
|
68
|
-
export async function embed(
|
|
69
|
-
texts: string[],
|
|
70
|
-
config: Required<BotholomewConfig>,
|
|
71
|
-
): Promise<number[][]> {
|
|
72
|
-
if (texts.length === 0) return [];
|
|
73
|
-
|
|
74
|
-
const extractor = await getPipeline(config.embedding_model);
|
|
75
|
-
const output = await extractor(texts, { pooling: "mean", normalize: true });
|
|
76
|
-
const data = output.tolist() as number[][];
|
|
77
|
-
|
|
78
|
-
if (data[0] && data[0].length !== config.embedding_dimension) {
|
|
79
|
-
throw new Error(
|
|
80
|
-
`Embedding model ${config.embedding_model} returned ${data[0].length}-dim vectors, but embedding_dimension is set to ${config.embedding_dimension}. Update embedding_dimension in config and re-embed.`,
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return data;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Embed a single text string.
|
|
89
|
-
*/
|
|
90
|
-
export async function embedSingle(
|
|
91
|
-
text: string,
|
|
92
|
-
config: Required<BotholomewConfig>,
|
|
93
|
-
): Promise<number[]> {
|
|
94
|
-
const results = await embed([text], config);
|
|
95
|
-
const vec = results[0];
|
|
96
|
-
if (!vec) throw new Error("embed returned empty results");
|
|
97
|
-
return vec;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export type { EmbedFn };
|
package/src/context/embedder.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// Re-exports the real embedder implementation from `embedder-impl.ts`.
|
|
2
|
-
//
|
|
3
|
-
// Why the indirection: tests that touch code importing from this file (e.g.,
|
|
4
|
-
// `src/chat/agent.ts`, `src/worker/prompt.ts`) use Bun's `mock.module()` to
|
|
5
|
-
// stub the embedder so they don't hit OpenAI. Bun's module mocks are
|
|
6
|
-
// process-wide and can leak into subsequent test files. By keeping the real
|
|
7
|
-
// implementation in `embedder-impl.ts`, `test/context/embedder.test.ts` can
|
|
8
|
-
// import the real embedder from a path that nothing mocks.
|
|
9
|
-
export * from "./embedder-impl.ts";
|