botholomew 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.
Files changed (62) hide show
  1. package/package.json +42 -0
  2. package/src/cli.ts +45 -0
  3. package/src/commands/chat.ts +11 -0
  4. package/src/commands/check-update.ts +62 -0
  5. package/src/commands/context.ts +27 -0
  6. package/src/commands/daemon.ts +61 -0
  7. package/src/commands/init.ts +19 -0
  8. package/src/commands/mcpx.ts +29 -0
  9. package/src/commands/task.ts +126 -0
  10. package/src/commands/tools.ts +257 -0
  11. package/src/commands/upgrade.ts +185 -0
  12. package/src/config/loader.ts +31 -0
  13. package/src/config/schemas.ts +15 -0
  14. package/src/constants.ts +44 -0
  15. package/src/daemon/index.ts +39 -0
  16. package/src/daemon/llm.ts +186 -0
  17. package/src/daemon/prompt.ts +55 -0
  18. package/src/daemon/run.ts +14 -0
  19. package/src/daemon/spawn.ts +38 -0
  20. package/src/daemon/tick.ts +70 -0
  21. package/src/db/connection.ts +32 -0
  22. package/src/db/context.ts +415 -0
  23. package/src/db/embeddings.ts +22 -0
  24. package/src/db/schedules.ts +17 -0
  25. package/src/db/schema.ts +66 -0
  26. package/src/db/sql/1-core_tables.sql +53 -0
  27. package/src/db/sql/2-logging_tables.sql +24 -0
  28. package/src/db/sql/3-daemon_state.sql +5 -0
  29. package/src/db/tasks.ts +194 -0
  30. package/src/db/threads.ts +202 -0
  31. package/src/db/uuid.ts +1 -0
  32. package/src/init/index.ts +84 -0
  33. package/src/init/templates.ts +48 -0
  34. package/src/tools/dir/create.ts +39 -0
  35. package/src/tools/dir/list.ts +87 -0
  36. package/src/tools/dir/size.ts +45 -0
  37. package/src/tools/dir/tree.ts +91 -0
  38. package/src/tools/file/copy.ts +30 -0
  39. package/src/tools/file/count-lines.ts +26 -0
  40. package/src/tools/file/delete.ts +43 -0
  41. package/src/tools/file/edit.ts +40 -0
  42. package/src/tools/file/exists.ts +23 -0
  43. package/src/tools/file/info.ts +50 -0
  44. package/src/tools/file/move.ts +29 -0
  45. package/src/tools/file/read.ts +40 -0
  46. package/src/tools/file/write.ts +90 -0
  47. package/src/tools/registry.ts +53 -0
  48. package/src/tools/search/grep.ts +94 -0
  49. package/src/tools/search/semantic.ts +40 -0
  50. package/src/tools/task/complete.ts +23 -0
  51. package/src/tools/task/create.ts +42 -0
  52. package/src/tools/task/fail.ts +22 -0
  53. package/src/tools/task/wait.ts +23 -0
  54. package/src/tools/tool.ts +73 -0
  55. package/src/tui/App.tsx +14 -0
  56. package/src/types/istextorbinary.d.ts +10 -0
  57. package/src/update/background.ts +89 -0
  58. package/src/update/cache.ts +40 -0
  59. package/src/update/checker.ts +133 -0
  60. package/src/utils/frontmatter.ts +24 -0
  61. package/src/utils/logger.ts +29 -0
  62. package/src/utils/pid.ts +55 -0
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+ import { listContextItemsByPrefix } from "../../db/context.ts";
3
+ import type { ToolDefinition } from "../tool.ts";
4
+
5
+ const DEFAULT_MAX_ITEMS = 200;
6
+
7
+ const inputSchema = z.object({
8
+ path: z
9
+ .string()
10
+ .optional()
11
+ .describe("Root path for the tree (defaults to /)"),
12
+ max_items: z
13
+ .number()
14
+ .optional()
15
+ .default(DEFAULT_MAX_ITEMS)
16
+ .describe(
17
+ `Maximum number of items to include (defaults to ${DEFAULT_MAX_ITEMS})`,
18
+ ),
19
+ });
20
+
21
+ const outputSchema = z.object({
22
+ tree: z.string(),
23
+ });
24
+
25
+ export const dirTreeTool = {
26
+ name: "dir_tree",
27
+ description:
28
+ "Render a directory as a markdown-style tree in the virtual filesystem.",
29
+ group: "dir",
30
+ inputSchema,
31
+ outputSchema,
32
+ execute: async (input, ctx) => {
33
+ const path = input.path ?? "/";
34
+ const maxItems = input.max_items ?? DEFAULT_MAX_ITEMS;
35
+ const items = await listContextItemsByPrefix(ctx.conn, path, {
36
+ recursive: true,
37
+ limit: maxItems,
38
+ });
39
+
40
+ if (items.length === 0) {
41
+ return { tree: `${path}\n (empty)` };
42
+ }
43
+
44
+ const normalizedPath = path.endsWith("/") ? path : `${path}/`;
45
+
46
+ // Build tree structure
47
+ const lines: string[] = [path];
48
+
49
+ // Collect all paths and sort
50
+ const paths = items.map((i) => i.context_path).sort();
51
+
52
+ // Collect all directory prefixes
53
+ const dirSet = new Set<string>();
54
+ for (const p of paths) {
55
+ const relative = p.slice(normalizedPath.length);
56
+ const parts = relative.split("/");
57
+ for (let i = 1; i < parts.length; i++) {
58
+ dirSet.add(parts.slice(0, i).join("/"));
59
+ }
60
+ }
61
+
62
+ // Merge dirs and files, sort
63
+ const allEntries = [
64
+ ...Array.from(dirSet).map((d) => ({ path: d, isDir: true })),
65
+ ...paths.map((p) => ({
66
+ path: p.slice(normalizedPath.length),
67
+ isDir: false,
68
+ })),
69
+ ].sort((a, b) => a.path.localeCompare(b.path));
70
+
71
+ for (let i = 0; i < allEntries.length; i++) {
72
+ const entry = allEntries[i];
73
+ if (!entry) continue;
74
+ const depth = entry.path.split("/").length - 1;
75
+ const isLast =
76
+ i === allEntries.length - 1 ||
77
+ (allEntries[i + 1]?.path.split("/").length ?? 0) - 1 <= depth;
78
+ const prefix = isLast ? "└── " : "├── ";
79
+ const indent = "│ ".repeat(depth);
80
+ const name = entry.path.split("/").pop() ?? "";
81
+ const suffix = entry.isDir ? "/" : "";
82
+ lines.push(`${indent}${prefix}${name}${suffix}`);
83
+ }
84
+
85
+ if (items.length >= maxItems) {
86
+ lines.push(`... (truncated at ${maxItems} items)`);
87
+ }
88
+
89
+ return { tree: lines.join("\n") };
90
+ },
91
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+ import { contextPathExists, copyContextItem } from "../../db/context.ts";
3
+ import type { ToolDefinition } from "../tool.ts";
4
+
5
+ const inputSchema = z.object({
6
+ src: z.string().describe("Source file path"),
7
+ dst: z.string().describe("Destination file path"),
8
+ overwrite: z.boolean().optional().describe("Overwrite if destination exists"),
9
+ });
10
+
11
+ const outputSchema = z.object({
12
+ id: z.string(),
13
+ path: z.string(),
14
+ });
15
+
16
+ export const fileCopyTool = {
17
+ name: "file_copy",
18
+ description: "Copy a file in the virtual filesystem.",
19
+ group: "file",
20
+ inputSchema,
21
+ outputSchema,
22
+ execute: async (input, ctx) => {
23
+ if (!input.overwrite && (await contextPathExists(ctx.conn, input.dst))) {
24
+ throw new Error(`Destination already exists: ${input.dst}`);
25
+ }
26
+
27
+ const item = await copyContextItem(ctx.conn, input.src, input.dst);
28
+ return { id: item.id, path: item.context_path };
29
+ },
30
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ import { getContextItemByPath } from "../../db/context.ts";
3
+ import type { ToolDefinition } from "../tool.ts";
4
+
5
+ const inputSchema = z.object({
6
+ path: z.string().describe("File path"),
7
+ });
8
+
9
+ const outputSchema = z.object({
10
+ lines: z.number(),
11
+ });
12
+
13
+ export const fileCountLinesTool = {
14
+ name: "file_count_lines",
15
+ description: "Count the number of lines in a text file.",
16
+ group: "file",
17
+ inputSchema,
18
+ outputSchema,
19
+ execute: async (input, ctx) => {
20
+ const item = await getContextItemByPath(ctx.conn, input.path);
21
+ if (!item) throw new Error(`Not found: ${input.path}`);
22
+ if (item.content == null) throw new Error(`No text content: ${input.path}`);
23
+
24
+ return { lines: item.content.split("\n").length };
25
+ },
26
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ import {
3
+ deleteContextItemByPath,
4
+ deleteContextItemsByPrefix,
5
+ } from "../../db/context.ts";
6
+ import type { ToolDefinition } from "../tool.ts";
7
+
8
+ const inputSchema = z.object({
9
+ path: z.string().describe("Path to delete"),
10
+ recursive: z
11
+ .boolean()
12
+ .optional()
13
+ .describe("Delete all items under this path prefix"),
14
+ force: z
15
+ .boolean()
16
+ .optional()
17
+ .describe("Do not error if the path does not exist"),
18
+ });
19
+
20
+ const outputSchema = z.object({
21
+ deleted: z.number(),
22
+ });
23
+
24
+ export const fileDeleteTool = {
25
+ name: "file_delete",
26
+ description: "Delete a file or directory from the virtual filesystem.",
27
+ group: "file",
28
+ inputSchema,
29
+ outputSchema,
30
+ execute: async (input, ctx) => {
31
+ if (input.recursive) {
32
+ const count = await deleteContextItemsByPrefix(ctx.conn, input.path);
33
+ const exact = await deleteContextItemByPath(ctx.conn, input.path);
34
+ return { deleted: count + (exact ? 1 : 0) };
35
+ }
36
+
37
+ const deleted = await deleteContextItemByPath(ctx.conn, input.path);
38
+ if (!deleted && !input.force) {
39
+ throw new Error(`Not found: ${input.path}`);
40
+ }
41
+ return { deleted: deleted ? 1 : 0 };
42
+ },
43
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ import { applyPatchesToContextItem } from "../../db/context.ts";
3
+ import type { ToolDefinition } from "../tool.ts";
4
+
5
+ const PatchSchema = z.object({
6
+ start_line: z.number().describe("1-based inclusive start line"),
7
+ end_line: z
8
+ .number()
9
+ .describe("1-based inclusive end line (0 to insert without replacing)"),
10
+ content: z
11
+ .string()
12
+ .describe("Replacement text (empty string to delete lines)"),
13
+ });
14
+
15
+ const inputSchema = z.object({
16
+ path: z.string().describe("File path to edit"),
17
+ patches: z.array(PatchSchema).describe("Patches to apply"),
18
+ });
19
+
20
+ const outputSchema = z.object({
21
+ applied: z.number(),
22
+ content: z.string(),
23
+ });
24
+
25
+ export const fileEditTool = {
26
+ name: "file_edit",
27
+ description:
28
+ "Apply git-style patches to a file. Each patch specifies a line range to replace.",
29
+ group: "file",
30
+ inputSchema,
31
+ outputSchema,
32
+ execute: async (input, ctx) => {
33
+ const { item, applied } = await applyPatchesToContextItem(
34
+ ctx.conn,
35
+ input.path,
36
+ input.patches,
37
+ );
38
+ return { applied, content: item.content ?? "" };
39
+ },
40
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import { contextPathExists } from "../../db/context.ts";
3
+ import type { ToolDefinition } from "../tool.ts";
4
+
5
+ const inputSchema = z.object({
6
+ path: z.string().describe("File path to check"),
7
+ });
8
+
9
+ const outputSchema = z.object({
10
+ exists: z.boolean(),
11
+ });
12
+
13
+ export const fileExistsTool = {
14
+ name: "file_exists",
15
+ description: "Check if a file exists in the virtual filesystem.",
16
+ group: "file",
17
+ inputSchema,
18
+ outputSchema,
19
+ execute: async (input, ctx) => {
20
+ const exists = await contextPathExists(ctx.conn, input.path);
21
+ return { exists };
22
+ },
23
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+ import { getContextItemByPath } from "../../db/context.ts";
3
+ import type { ToolDefinition } from "../tool.ts";
4
+
5
+ const inputSchema = z.object({
6
+ path: z.string().describe("File path"),
7
+ });
8
+
9
+ const outputSchema = z.object({
10
+ id: z.string(),
11
+ title: z.string(),
12
+ description: z.string(),
13
+ mime_type: z.string(),
14
+ is_textual: z.boolean(),
15
+ size: z.number(),
16
+ lines: z.number(),
17
+ source_path: z.string().nullable(),
18
+ context_path: z.string(),
19
+ indexed_at: z.string().nullable(),
20
+ created_at: z.string(),
21
+ updated_at: z.string(),
22
+ });
23
+
24
+ export const fileInfoTool = {
25
+ name: "file_info",
26
+ description: "Show file metadata (size, MIME type, line count, etc.).",
27
+ group: "file",
28
+ inputSchema,
29
+ outputSchema,
30
+ execute: async (input, ctx) => {
31
+ const item = await getContextItemByPath(ctx.conn, input.path);
32
+ if (!item) throw new Error(`Not found: ${input.path}`);
33
+
34
+ const content = item.content ?? "";
35
+ return {
36
+ id: item.id,
37
+ title: item.title,
38
+ description: item.description,
39
+ mime_type: item.mime_type,
40
+ is_textual: item.is_textual,
41
+ size: content.length,
42
+ lines: content ? content.split("\n").length : 0,
43
+ source_path: item.source_path,
44
+ context_path: item.context_path,
45
+ indexed_at: item.indexed_at?.toISOString() ?? null,
46
+ created_at: item.created_at.toISOString(),
47
+ updated_at: item.updated_at.toISOString(),
48
+ };
49
+ },
50
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+ import { contextPathExists, moveContextItem } from "../../db/context.ts";
3
+ import type { ToolDefinition } from "../tool.ts";
4
+
5
+ const inputSchema = z.object({
6
+ src: z.string().describe("Source file path"),
7
+ dst: z.string().describe("Destination file path"),
8
+ overwrite: z.boolean().optional().describe("Overwrite if destination exists"),
9
+ });
10
+
11
+ const outputSchema = z.object({
12
+ path: z.string(),
13
+ });
14
+
15
+ export const fileMoveTool = {
16
+ name: "file_move",
17
+ description: "Move or rename a file in the virtual filesystem.",
18
+ group: "file",
19
+ inputSchema,
20
+ outputSchema,
21
+ execute: async (input, ctx) => {
22
+ if (!input.overwrite && (await contextPathExists(ctx.conn, input.dst))) {
23
+ throw new Error(`Destination already exists: ${input.dst}`);
24
+ }
25
+
26
+ await moveContextItem(ctx.conn, input.src, input.dst);
27
+ return { path: input.dst };
28
+ },
29
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ import { getContextItemByPath } from "../../db/context.ts";
3
+ import type { ToolDefinition } from "../tool.ts";
4
+
5
+ const inputSchema = z.object({
6
+ path: z.string().describe("File path to read"),
7
+ offset: z
8
+ .number()
9
+ .optional()
10
+ .describe("Line number to start reading from (1-based)"),
11
+ limit: z.number().optional().describe("Maximum number of lines to return"),
12
+ });
13
+
14
+ const outputSchema = z.object({
15
+ content: z.string(),
16
+ });
17
+
18
+ export const fileReadTool = {
19
+ name: "file_read",
20
+ description: "Read a file's contents from the virtual filesystem.",
21
+ group: "file",
22
+ inputSchema,
23
+ outputSchema,
24
+ execute: async (input, ctx) => {
25
+ const item = await getContextItemByPath(ctx.conn, input.path);
26
+ if (!item) throw new Error(`Not found: ${input.path}`);
27
+ if (item.content == null) throw new Error(`No text content: ${input.path}`);
28
+
29
+ let content = item.content;
30
+
31
+ if (input.offset || input.limit) {
32
+ const lines = content.split("\n");
33
+ const start = (input.offset ?? 1) - 1;
34
+ const end = input.limit ? start + input.limit : lines.length;
35
+ content = lines.slice(start, end).join("\n");
36
+ }
37
+
38
+ return { content };
39
+ },
40
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -0,0 +1,90 @@
1
+ import { isText } from "istextorbinary";
2
+ import { z } from "zod";
3
+ import {
4
+ createContextItem,
5
+ getContextItemByPath,
6
+ updateContextItem,
7
+ updateContextItemContent,
8
+ } from "../../db/context.ts";
9
+ import type { ToolDefinition } from "../tool.ts";
10
+
11
+ function mimeFromPath(path: string): string {
12
+ const type = Bun.file(path).type.split(";")[0];
13
+ return type ?? "application/octet-stream";
14
+ }
15
+
16
+ function isTextualPath(path: string): boolean {
17
+ const filename = path.split("/").pop() ?? path;
18
+ const result = isText(filename);
19
+ // isText returns null if it can't determine from filename alone — default to true
20
+ return result !== false;
21
+ }
22
+
23
+ const inputSchema = z.object({
24
+ path: z.string().describe("File path to write"),
25
+ content: z.string().describe("Text content to write"),
26
+ content_base64: z
27
+ .string()
28
+ .optional()
29
+ .describe(
30
+ "Base64-encoded binary content (used instead of content for binary files)",
31
+ ),
32
+ title: z
33
+ .string()
34
+ .optional()
35
+ .describe("Title for the file (defaults to filename)"),
36
+ description: z.string().optional().describe("Description of the file"),
37
+ });
38
+
39
+ const outputSchema = z.object({
40
+ id: z.string(),
41
+ path: z.string(),
42
+ });
43
+
44
+ export const fileWriteTool = {
45
+ name: "file_write",
46
+ description:
47
+ "Write content to a file in the virtual filesystem. Creates the file if it doesn't exist, or overwrites if it does.",
48
+ group: "file",
49
+ inputSchema,
50
+ outputSchema,
51
+ execute: async (input, ctx) => {
52
+ const mimeType = mimeFromPath(input.path);
53
+ const isTextual = isTextualPath(input.path);
54
+ const existing = await getContextItemByPath(ctx.conn, input.path);
55
+
56
+ if (existing) {
57
+ if (input.content_base64) {
58
+ // Binary update — store as content for now (DB blob support can be added later)
59
+ await updateContextItemContent(
60
+ ctx.conn,
61
+ input.path,
62
+ input.content_base64,
63
+ );
64
+ } else {
65
+ await updateContextItemContent(ctx.conn, input.path, input.content);
66
+ }
67
+ if (input.title || input.description) {
68
+ await updateContextItem(ctx.conn, existing.id, {
69
+ title: input.title,
70
+ description: input.description,
71
+ });
72
+ }
73
+ return { id: existing.id, path: input.path };
74
+ }
75
+
76
+ const title =
77
+ input.title ?? input.path.split("/").filter(Boolean).pop() ?? input.path;
78
+
79
+ const item = await createContextItem(ctx.conn, {
80
+ title,
81
+ description: input.description,
82
+ content: input.content_base64 ?? input.content,
83
+ contextPath: input.path,
84
+ mimeType,
85
+ isTextual,
86
+ });
87
+
88
+ return { id: item.id, path: item.context_path };
89
+ },
90
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -0,0 +1,53 @@
1
+ // Directory tools
2
+ import { dirCreateTool } from "./dir/create.ts";
3
+ import { dirListTool } from "./dir/list.ts";
4
+ import { dirSizeTool } from "./dir/size.ts";
5
+ import { dirTreeTool } from "./dir/tree.ts";
6
+ import { fileCopyTool } from "./file/copy.ts";
7
+ import { fileCountLinesTool } from "./file/count-lines.ts";
8
+ import { fileDeleteTool } from "./file/delete.ts";
9
+ import { fileEditTool } from "./file/edit.ts";
10
+ import { fileExistsTool } from "./file/exists.ts";
11
+ import { fileInfoTool } from "./file/info.ts";
12
+ import { fileMoveTool } from "./file/move.ts";
13
+ // File tools
14
+ import { fileReadTool } from "./file/read.ts";
15
+ import { fileWriteTool } from "./file/write.ts";
16
+ // Search tools
17
+ import { searchGrepTool } from "./search/grep.ts";
18
+ import { searchSemanticTool } from "./search/semantic.ts";
19
+ // Task tools
20
+ import { completeTaskTool } from "./task/complete.ts";
21
+ import { createTaskTool } from "./task/create.ts";
22
+ import { failTaskTool } from "./task/fail.ts";
23
+ import { waitTaskTool } from "./task/wait.ts";
24
+ import { registerTool } from "./tool.ts";
25
+
26
+ export function registerAllTools(): void {
27
+ // Task
28
+ registerTool(completeTaskTool);
29
+ registerTool(failTaskTool);
30
+ registerTool(waitTaskTool);
31
+ registerTool(createTaskTool);
32
+
33
+ // Directory
34
+ registerTool(dirCreateTool);
35
+ registerTool(dirListTool);
36
+ registerTool(dirTreeTool);
37
+ registerTool(dirSizeTool);
38
+
39
+ // File
40
+ registerTool(fileReadTool);
41
+ registerTool(fileWriteTool);
42
+ registerTool(fileEditTool);
43
+ registerTool(fileDeleteTool);
44
+ registerTool(fileCopyTool);
45
+ registerTool(fileMoveTool);
46
+ registerTool(fileInfoTool);
47
+ registerTool(fileExistsTool);
48
+ registerTool(fileCountLinesTool);
49
+
50
+ // Search
51
+ registerTool(searchGrepTool);
52
+ registerTool(searchSemanticTool);
53
+ }
@@ -0,0 +1,94 @@
1
+ import { z } from "zod";
2
+ import { listContextItemsByPrefix } from "../../db/context.ts";
3
+ import type { ToolDefinition } from "../tool.ts";
4
+
5
+ const GrepMatchSchema = z.object({
6
+ path: z.string(),
7
+ line: z.number(),
8
+ content: z.string(),
9
+ context_lines: z.array(z.string()),
10
+ });
11
+
12
+ const inputSchema = z.object({
13
+ pattern: z.string().describe("Regex pattern to search for"),
14
+ path: z
15
+ .string()
16
+ .optional()
17
+ .describe("Directory to search in (defaults to /)"),
18
+ glob: z
19
+ .string()
20
+ .optional()
21
+ .describe("Only search files matching this glob pattern"),
22
+ ignore_case: z.boolean().optional().describe("Case-insensitive search"),
23
+ context: z
24
+ .number()
25
+ .optional()
26
+ .describe("Number of context lines before and after each match"),
27
+ max_results: z
28
+ .number()
29
+ .optional()
30
+ .describe("Maximum number of matches to return"),
31
+ });
32
+
33
+ const outputSchema = z.object({
34
+ matches: z.array(GrepMatchSchema),
35
+ });
36
+
37
+ export const searchGrepTool = {
38
+ name: "search_grep",
39
+ description:
40
+ "Search file contents by regex pattern in the virtual filesystem.",
41
+ group: "search",
42
+ inputSchema,
43
+ outputSchema,
44
+ execute: async (input, ctx) => {
45
+ const searchPath = input.path ?? "/";
46
+ const items = await listContextItemsByPrefix(ctx.conn, searchPath, {
47
+ recursive: true,
48
+ });
49
+
50
+ const flags = input.ignore_case ? "gi" : "g";
51
+ const regex = new RegExp(input.pattern, flags);
52
+ const globRegex = input.glob ? globToRegex(input.glob) : null;
53
+ const contextLines = input.context ?? 0;
54
+ const maxResults = input.max_results ?? 100;
55
+
56
+ const matches: z.infer<typeof GrepMatchSchema>[] = [];
57
+
58
+ for (const item of items) {
59
+ if (item.content == null) continue;
60
+
61
+ if (globRegex) {
62
+ const filename = item.context_path.split("/").pop() ?? "";
63
+ if (!globRegex.test(filename)) continue;
64
+ }
65
+
66
+ const lines = item.content.split("\n");
67
+ for (let i = 0; i < lines.length; i++) {
68
+ regex.lastIndex = 0;
69
+ const line = lines[i];
70
+ if (line !== undefined && regex.test(line)) {
71
+ const start = Math.max(0, i - contextLines);
72
+ const end = Math.min(lines.length, i + contextLines + 1);
73
+ matches.push({
74
+ path: item.context_path,
75
+ line: i + 1,
76
+ content: line,
77
+ context_lines: lines.slice(start, end),
78
+ });
79
+ if (matches.length >= maxResults) return { matches };
80
+ }
81
+ }
82
+ }
83
+
84
+ return { matches };
85
+ },
86
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
87
+
88
+ function globToRegex(glob: string): RegExp {
89
+ const escaped = glob
90
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
91
+ .replace(/\*/g, ".*")
92
+ .replace(/\?/g, ".");
93
+ return new RegExp(`^${escaped}$`, "i");
94
+ }
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ import type { ToolDefinition } from "../tool.ts";
3
+
4
+ const inputSchema = z.object({
5
+ query: z.string().describe("Natural language search query"),
6
+ top_k: z
7
+ .number()
8
+ .optional()
9
+ .default(10)
10
+ .describe("Maximum number of results to return (defaults to 10)"),
11
+ threshold: z
12
+ .number()
13
+ .optional()
14
+ .describe("Minimum similarity score (0-1) to include in results"),
15
+ });
16
+
17
+ const outputSchema = z.object({
18
+ results: z.array(
19
+ z.object({
20
+ path: z.string(),
21
+ title: z.string(),
22
+ score: z.number(),
23
+ snippet: z.string(),
24
+ }),
25
+ ),
26
+ });
27
+
28
+ export const searchSemanticTool = {
29
+ name: "search_semantic",
30
+ description:
31
+ "Semantic search over indexed files using vector embeddings. Finds conceptually related content, not just keyword matches.",
32
+ group: "search",
33
+ inputSchema,
34
+ outputSchema,
35
+ execute: async () => {
36
+ throw new Error(
37
+ "Semantic search is not yet available — requires the embeddings pipeline (M2)",
38
+ );
39
+ },
40
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import type { ToolDefinition } from "../tool.ts";
3
+
4
+ const inputSchema = z.object({
5
+ summary: z.string().describe("Summary of work done"),
6
+ });
7
+
8
+ const outputSchema = z.object({
9
+ message: z.string(),
10
+ });
11
+
12
+ export const completeTaskTool = {
13
+ name: "complete_task",
14
+ description:
15
+ "Mark the current task as complete with a summary of what was accomplished.",
16
+ group: "task",
17
+ terminal: true,
18
+ inputSchema,
19
+ outputSchema,
20
+ execute: async (input) => ({
21
+ message: `Task completed: ${input.summary}`,
22
+ }),
23
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;