botholomew 0.8.1 → 0.8.3
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/package.json +1 -1
- package/src/context/embedder-impl.ts +69 -0
- package/src/context/embedder.ts +9 -69
- package/src/db/context.ts +16 -4
- package/src/tools/context/refresh.ts +1 -1
- package/src/tools/context/search.ts +2 -1
- package/src/tools/dir/create.ts +2 -1
- package/src/tools/dir/list.ts +2 -1
- package/src/tools/dir/size.ts +2 -1
- package/src/tools/dir/tree.ts +1 -1
- package/src/tools/file/copy.ts +1 -1
- package/src/tools/file/count-lines.ts +2 -1
- package/src/tools/file/delete.ts +2 -1
- package/src/tools/file/edit.ts +1 -1
- package/src/tools/file/exists.ts +2 -1
- package/src/tools/file/info.ts +1 -1
- package/src/tools/file/move.ts +2 -1
- package/src/tools/file/read.ts +2 -1
- package/src/tools/file/write.ts +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
2
|
+
|
|
3
|
+
type EmbedFn = (
|
|
4
|
+
texts: string[],
|
|
5
|
+
config: Required<BotholomewConfig>,
|
|
6
|
+
) => Promise<number[][]>;
|
|
7
|
+
|
|
8
|
+
interface OpenAIEmbeddingResponse {
|
|
9
|
+
data: { embedding: number[]; index: number }[];
|
|
10
|
+
usage: { total_tokens: number };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Embed multiple texts using the OpenAI embeddings API.
|
|
15
|
+
* Returns an array of float vectors with the configured dimension.
|
|
16
|
+
*/
|
|
17
|
+
export async function embed(
|
|
18
|
+
texts: string[],
|
|
19
|
+
config: Required<BotholomewConfig>,
|
|
20
|
+
): Promise<number[][]> {
|
|
21
|
+
if (texts.length === 0) return [];
|
|
22
|
+
|
|
23
|
+
if (!config.openai_api_key) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"OpenAI API key is required for embeddings. Set openai_api_key in config or OPENAI_API_KEY env var.",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const response = await fetch("https://api.openai.com/v1/embeddings", {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
Authorization: `Bearer ${config.openai_api_key}`,
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
input: texts,
|
|
37
|
+
model: config.embedding_model,
|
|
38
|
+
dimensions: config.embedding_dimension,
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const body = await response.text();
|
|
44
|
+
throw new Error(
|
|
45
|
+
`OpenAI embeddings API error (${response.status}): ${body}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const result = (await response.json()) as OpenAIEmbeddingResponse;
|
|
50
|
+
|
|
51
|
+
// Sort by index to ensure order matches input
|
|
52
|
+
const sorted = result.data.sort((a, b) => a.index - b.index);
|
|
53
|
+
return sorted.map((d) => d.embedding);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Embed a single text string.
|
|
58
|
+
*/
|
|
59
|
+
export async function embedSingle(
|
|
60
|
+
text: string,
|
|
61
|
+
config: Required<BotholomewConfig>,
|
|
62
|
+
): Promise<number[]> {
|
|
63
|
+
const results = await embed([text], config);
|
|
64
|
+
const vec = results[0];
|
|
65
|
+
if (!vec) throw new Error("embed returned empty results");
|
|
66
|
+
return vec;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type { EmbedFn };
|
package/src/context/embedder.ts
CHANGED
|
@@ -1,69 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
usage: { total_tokens: number };
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Embed multiple texts using the OpenAI embeddings API.
|
|
15
|
-
* Returns an array of float vectors with the configured dimension.
|
|
16
|
-
*/
|
|
17
|
-
export async function embed(
|
|
18
|
-
texts: string[],
|
|
19
|
-
config: Required<BotholomewConfig>,
|
|
20
|
-
): Promise<number[][]> {
|
|
21
|
-
if (texts.length === 0) return [];
|
|
22
|
-
|
|
23
|
-
if (!config.openai_api_key) {
|
|
24
|
-
throw new Error(
|
|
25
|
-
"OpenAI API key is required for embeddings. Set openai_api_key in config or OPENAI_API_KEY env var.",
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const response = await fetch("https://api.openai.com/v1/embeddings", {
|
|
30
|
-
method: "POST",
|
|
31
|
-
headers: {
|
|
32
|
-
Authorization: `Bearer ${config.openai_api_key}`,
|
|
33
|
-
"Content-Type": "application/json",
|
|
34
|
-
},
|
|
35
|
-
body: JSON.stringify({
|
|
36
|
-
input: texts,
|
|
37
|
-
model: config.embedding_model,
|
|
38
|
-
dimensions: config.embedding_dimension,
|
|
39
|
-
}),
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
if (!response.ok) {
|
|
43
|
-
const body = await response.text();
|
|
44
|
-
throw new Error(
|
|
45
|
-
`OpenAI embeddings API error (${response.status}): ${body}`,
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const result = (await response.json()) as OpenAIEmbeddingResponse;
|
|
50
|
-
|
|
51
|
-
// Sort by index to ensure order matches input
|
|
52
|
-
const sorted = result.data.sort((a, b) => a.index - b.index);
|
|
53
|
-
return sorted.map((d) => d.embedding);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Embed a single text string.
|
|
58
|
-
*/
|
|
59
|
-
export async function embedSingle(
|
|
60
|
-
text: string,
|
|
61
|
-
config: Required<BotholomewConfig>,
|
|
62
|
-
): Promise<number[]> {
|
|
63
|
-
const results = await embed([text], config);
|
|
64
|
-
const vec = results[0];
|
|
65
|
-
if (!vec) throw new Error("embed returned empty results");
|
|
66
|
-
return vec;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export type { EmbedFn };
|
|
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";
|
package/src/db/context.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolve as resolvePath } from "node:path";
|
|
1
2
|
import type { DbConnection } from "./connection.ts";
|
|
2
3
|
import { buildSetClauses, buildWhereClause, sanitizeInt } from "./query.ts";
|
|
3
4
|
import { isUuid, uuidv7 } from "./uuid.ts";
|
|
@@ -193,15 +194,26 @@ export async function getContextItemBySourcePath(
|
|
|
193
194
|
}
|
|
194
195
|
|
|
195
196
|
/**
|
|
196
|
-
* Look up a context item by UUID
|
|
197
|
+
* Look up a context item by UUID, `context_path`, or `source_path`.
|
|
198
|
+
*
|
|
199
|
+
* `source_path` fallbacks let users pass the same argument they used for
|
|
200
|
+
* `context add` (e.g. a bare `README.md`) to management commands like
|
|
201
|
+
* `context refresh` / `context chunks`. Relative file paths are resolved
|
|
202
|
+
* against `process.cwd()` to match the absolute `source_path` stored on add.
|
|
197
203
|
*/
|
|
198
204
|
export async function resolveContextItem(
|
|
199
205
|
db: DbConnection,
|
|
200
206
|
pathOrId: string,
|
|
201
207
|
): Promise<ContextItem | null> {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
208
|
+
if (isUuid(pathOrId)) return getContextItem(db, pathOrId);
|
|
209
|
+
|
|
210
|
+
const byContextPath = await getContextItemByPath(db, pathOrId);
|
|
211
|
+
if (byContextPath) return byContextPath;
|
|
212
|
+
|
|
213
|
+
const byUrl = await getContextItemBySourcePath(db, pathOrId, "url");
|
|
214
|
+
if (byUrl) return byUrl;
|
|
215
|
+
|
|
216
|
+
return getContextItemBySourcePath(db, resolvePath(pathOrId), "file");
|
|
205
217
|
}
|
|
206
218
|
|
|
207
219
|
/**
|
|
@@ -59,7 +59,7 @@ const empty = {
|
|
|
59
59
|
export const contextRefreshTool = {
|
|
60
60
|
name: "context_refresh",
|
|
61
61
|
description:
|
|
62
|
-
"Re-read source files from disk / re-fetch source URLs, update stored content if it changed, and re-embed only changed items. Use `path` for a single item or subtree, or `all: true` for every sourced item. Items without a source_path are skipped. URL fetches use the project's MCPX client when available and fall back to plain HTTP.",
|
|
62
|
+
"[[ bash equivalent command: curl ]] Re-read source files from disk / re-fetch source URLs, update stored content if it changed, and re-embed only changed items. Use `path` for a single item or subtree, or `all: true` for every sourced item. Items without a source_path are skipped. URL fetches use the project's MCPX client when available and fall back to plain HTTP.",
|
|
63
63
|
group: "context",
|
|
64
64
|
inputSchema,
|
|
65
65
|
outputSchema,
|
|
@@ -24,7 +24,8 @@ const outputSchema = z.object({
|
|
|
24
24
|
|
|
25
25
|
export const contextSearchTool = {
|
|
26
26
|
name: "context_search",
|
|
27
|
-
description:
|
|
27
|
+
description:
|
|
28
|
+
"[[ bash equivalent command: grep -r ]] Search context by keyword.",
|
|
28
29
|
group: "context",
|
|
29
30
|
inputSchema,
|
|
30
31
|
outputSchema,
|
package/src/tools/dir/create.ts
CHANGED
|
@@ -18,7 +18,8 @@ const outputSchema = z.object({
|
|
|
18
18
|
|
|
19
19
|
export const contextCreateDirTool = {
|
|
20
20
|
name: "context_create_dir",
|
|
21
|
-
description:
|
|
21
|
+
description:
|
|
22
|
+
"[[ bash equivalent command: mkdir -p ]] Create a directory in context.",
|
|
22
23
|
group: "context",
|
|
23
24
|
inputSchema,
|
|
24
25
|
outputSchema,
|
package/src/tools/dir/list.ts
CHANGED
|
@@ -38,7 +38,8 @@ const outputSchema = z.object({
|
|
|
38
38
|
|
|
39
39
|
export const contextListDirTool = {
|
|
40
40
|
name: "context_list_dir",
|
|
41
|
-
description:
|
|
41
|
+
description:
|
|
42
|
+
"[[ bash equivalent command: ls ]] List directory contents in context.",
|
|
42
43
|
group: "context",
|
|
43
44
|
inputSchema,
|
|
44
45
|
outputSchema,
|
package/src/tools/dir/size.ts
CHANGED
|
@@ -26,7 +26,8 @@ const outputSchema = z.object({
|
|
|
26
26
|
|
|
27
27
|
export const contextDirSizeTool = {
|
|
28
28
|
name: "context_dir_size",
|
|
29
|
-
description:
|
|
29
|
+
description:
|
|
30
|
+
"[[ bash equivalent command: du -s ]] Get the total size of context items in a directory.",
|
|
30
31
|
group: "context",
|
|
31
32
|
inputSchema,
|
|
32
33
|
outputSchema,
|
package/src/tools/dir/tree.ts
CHANGED
|
@@ -66,7 +66,7 @@ type TreeEntry = DirNode | FileNode;
|
|
|
66
66
|
export const contextTreeTool = {
|
|
67
67
|
name: "context_tree",
|
|
68
68
|
description:
|
|
69
|
-
"Explore your context filesystem with a bird's-eye view — shows many paths across nested directories in one call. Reach for this first when you need to discover what content exists before reading a specific file (context_read) or running a keyword search (context_search). Returns a markdown-style tree; tune max_depth and items_per_dir to bound output, or pass a deeper path to drill into a subtree.",
|
|
69
|
+
"[[ bash equivalent command: tree ]] Explore your context filesystem with a bird's-eye view — shows many paths across nested directories in one call. Reach for this first when you need to discover what content exists before reading a specific file (context_read) or running a keyword search (context_search). Returns a markdown-style tree; tune max_depth and items_per_dir to bound output, or pass a deeper path to drill into a subtree.",
|
|
70
70
|
group: "context",
|
|
71
71
|
inputSchema,
|
|
72
72
|
outputSchema,
|
package/src/tools/file/copy.ts
CHANGED
|
@@ -20,7 +20,7 @@ const outputSchema = z.object({
|
|
|
20
20
|
|
|
21
21
|
export const contextCopyTool = {
|
|
22
22
|
name: "context_copy",
|
|
23
|
-
description: "Copy a context item.",
|
|
23
|
+
description: "[[ bash equivalent command: cp ]] Copy a context item.",
|
|
24
24
|
group: "context",
|
|
25
25
|
inputSchema,
|
|
26
26
|
outputSchema,
|
|
@@ -13,7 +13,8 @@ const outputSchema = z.object({
|
|
|
13
13
|
|
|
14
14
|
export const contextCountLinesTool = {
|
|
15
15
|
name: "context_count_lines",
|
|
16
|
-
description:
|
|
16
|
+
description:
|
|
17
|
+
"[[ bash equivalent command: wc -l ]] Count the number of lines in a text context item.",
|
|
17
18
|
group: "context",
|
|
18
19
|
inputSchema,
|
|
19
20
|
outputSchema,
|
package/src/tools/file/delete.ts
CHANGED
|
@@ -24,7 +24,8 @@ const outputSchema = z.object({
|
|
|
24
24
|
|
|
25
25
|
export const contextDeleteTool = {
|
|
26
26
|
name: "context_delete",
|
|
27
|
-
description:
|
|
27
|
+
description:
|
|
28
|
+
"[[ bash equivalent command: rm -r ]] Delete a context item or directory.",
|
|
28
29
|
group: "context",
|
|
29
30
|
inputSchema,
|
|
30
31
|
outputSchema,
|
package/src/tools/file/edit.ts
CHANGED
|
@@ -27,7 +27,7 @@ const outputSchema = z.object({
|
|
|
27
27
|
export const contextEditTool = {
|
|
28
28
|
name: "context_edit",
|
|
29
29
|
description:
|
|
30
|
-
"Apply git-style patches to a context item. Each patch specifies a line range to replace.",
|
|
30
|
+
"[[ bash equivalent command: patch ]] Apply git-style patches to a context item. Each patch specifies a line range to replace.",
|
|
31
31
|
group: "context",
|
|
32
32
|
inputSchema,
|
|
33
33
|
outputSchema,
|
package/src/tools/file/exists.ts
CHANGED
|
@@ -13,7 +13,8 @@ const outputSchema = z.object({
|
|
|
13
13
|
|
|
14
14
|
export const contextExistsTool = {
|
|
15
15
|
name: "context_exists",
|
|
16
|
-
description:
|
|
16
|
+
description:
|
|
17
|
+
"[[ bash equivalent command: test -e ]] Check if a context item exists.",
|
|
17
18
|
group: "context",
|
|
18
19
|
inputSchema,
|
|
19
20
|
outputSchema,
|
package/src/tools/file/info.ts
CHANGED
|
@@ -25,7 +25,7 @@ const outputSchema = z.object({
|
|
|
25
25
|
export const contextInfoTool = {
|
|
26
26
|
name: "context_info",
|
|
27
27
|
description:
|
|
28
|
-
"Show context item metadata
|
|
28
|
+
"[[ bash equivalent command: stat ]] Show context item metadata: size, MIME type, line count, etc.",
|
|
29
29
|
group: "context",
|
|
30
30
|
inputSchema,
|
|
31
31
|
outputSchema,
|
package/src/tools/file/move.ts
CHANGED
|
@@ -19,7 +19,8 @@ const outputSchema = z.object({
|
|
|
19
19
|
|
|
20
20
|
export const contextMoveTool = {
|
|
21
21
|
name: "context_move",
|
|
22
|
-
description:
|
|
22
|
+
description:
|
|
23
|
+
"[[ bash equivalent command: mv ]] Move or rename a context item.",
|
|
23
24
|
group: "context",
|
|
24
25
|
inputSchema,
|
|
25
26
|
outputSchema,
|
package/src/tools/file/read.ts
CHANGED
|
@@ -18,7 +18,8 @@ const outputSchema = z.object({
|
|
|
18
18
|
|
|
19
19
|
export const contextReadTool = {
|
|
20
20
|
name: "context_read",
|
|
21
|
-
description:
|
|
21
|
+
description:
|
|
22
|
+
"[[ bash equivalent command: cat ]] Read a context item's contents.",
|
|
22
23
|
group: "context",
|
|
23
24
|
inputSchema,
|
|
24
25
|
outputSchema,
|
package/src/tools/file/write.ts
CHANGED
|
@@ -54,7 +54,7 @@ const outputSchema = z.object({
|
|
|
54
54
|
export const contextWriteTool = {
|
|
55
55
|
name: "context_write",
|
|
56
56
|
description:
|
|
57
|
-
"Write content to a context item. By default, fails if the path already exists — pass on_conflict='overwrite' to replace.",
|
|
57
|
+
"[[ bash equivalent command: tee ]] Write content to a context item. By default, fails if the path already exists — pass on_conflict='overwrite' to replace.",
|
|
58
58
|
group: "context",
|
|
59
59
|
inputSchema,
|
|
60
60
|
outputSchema,
|