botholomew 0.12.5 → 0.14.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 +91 -68
- package/package.json +2 -2
- package/src/chat/agent.ts +59 -86
- package/src/chat/session.ts +29 -25
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +178 -926
- package/src/commands/db.ts +9 -13
- package/src/commands/init.ts +4 -1
- package/src/commands/nuke.ts +57 -90
- package/src/commands/schedule.ts +103 -124
- package/src/commands/skill.ts +2 -2
- package/src/commands/task.ts +86 -95
- package/src/commands/thread.ts +107 -112
- package/src/commands/worker.ts +88 -88
- package/src/constants.ts +93 -16
- package/src/context/capabilities.ts +10 -10
- package/src/context/fetcher.ts +9 -10
- package/src/context/reindex.ts +189 -0
- package/src/context/store.ts +803 -0
- package/src/db/doctor.ts +1 -8
- package/src/db/embeddings.ts +227 -175
- package/src/db/sql/19-disk_backed_index.sql +36 -0
- package/src/db/sql/20-drop_db_tables_for_files.sql +19 -0
- package/src/fs/atomic.ts +217 -0
- package/src/fs/compat.ts +86 -0
- package/src/fs/sandbox.ts +293 -0
- package/src/init/index.ts +69 -52
- package/src/init/templates.ts +1 -1
- package/src/mcpx/client.ts +1 -1
- package/src/schedules/schema.ts +19 -0
- package/src/schedules/store.ts +296 -0
- package/src/skills/commands.ts +1 -3
- package/src/tasks/schema.ts +47 -0
- package/src/tasks/store.ts +486 -0
- package/src/threads/store.ts +559 -0
- package/src/tools/capabilities/refresh.ts +42 -21
- package/src/tools/context/pipe.ts +15 -71
- package/src/tools/context/update-beliefs.ts +3 -3
- package/src/tools/context/update-goals.ts +3 -3
- package/src/tools/dir/create.ts +26 -23
- package/src/tools/dir/size.ts +46 -17
- package/src/tools/dir/tree.ts +74 -279
- package/src/tools/file/copy.ts +50 -24
- package/src/tools/file/count-lines.ts +34 -10
- package/src/tools/file/delete.ts +53 -23
- package/src/tools/file/edit.ts +39 -14
- package/src/tools/file/exists.ts +12 -26
- package/src/tools/file/info.ts +27 -85
- package/src/tools/file/move.ts +39 -24
- package/src/tools/file/read.ts +32 -80
- package/src/tools/file/write.ts +14 -91
- package/src/tools/registry.ts +8 -7
- package/src/tools/schedule/create.ts +2 -2
- package/src/tools/schedule/list.ts +7 -3
- package/src/tools/search/fuse.ts +12 -33
- package/src/tools/search/index.ts +36 -43
- package/src/tools/search/regexp.ts +29 -17
- package/src/tools/search/semantic.ts +137 -51
- package/src/tools/skill/delete.ts +1 -1
- package/src/tools/skill/list.ts +1 -1
- package/src/tools/skill/write.ts +1 -1
- package/src/tools/task/create.ts +41 -16
- package/src/tools/task/delete.ts +3 -3
- package/src/tools/task/list.ts +6 -3
- package/src/tools/task/update.ts +31 -9
- package/src/tools/task/view.ts +6 -6
- package/src/tools/thread/list.ts +2 -2
- package/src/tools/thread/search.ts +208 -0
- package/src/tools/thread/view.ts +50 -5
- package/src/tools/tool.ts +5 -0
- package/src/tools/util/sleep.ts +77 -0
- package/src/tools/worker/spawn.ts +28 -14
- package/src/tui/App.tsx +12 -19
- package/src/tui/components/ContextPanel.tsx +83 -316
- package/src/tui/components/SchedulePanel.tsx +34 -48
- package/src/tui/components/SleepProgress.tsx +70 -0
- package/src/tui/components/StatusBar.tsx +15 -15
- package/src/tui/components/TaskPanel.tsx +34 -38
- package/src/tui/components/ThreadPanel.tsx +29 -38
- package/src/tui/components/ToolCall.tsx +10 -0
- package/src/tui/components/WorkerPanel.tsx +21 -19
- package/src/tui/markdown.ts +2 -8
- package/src/utils/title.ts +5 -7
- package/src/utils/v7-date.ts +47 -0
- package/src/worker/heartbeat.ts +46 -24
- package/src/worker/index.ts +13 -15
- package/src/worker/llm.ts +30 -37
- package/src/worker/prompt.ts +19 -41
- package/src/worker/schedules.ts +48 -69
- package/src/worker/spawn.ts +11 -11
- package/src/worker/tick.ts +39 -43
- package/src/workers/store.ts +247 -0
- package/src/commands/tools.ts +0 -367
- package/src/context/describer.ts +0 -140
- package/src/context/drives.ts +0 -110
- package/src/context/ingest.ts +0 -162
- package/src/context/refresh.ts +0 -183
- package/src/db/context.ts +0 -637
- package/src/db/daemon-state.ts +0 -6
- package/src/db/reembed.ts +0 -113
- package/src/db/schedules.ts +0 -213
- package/src/db/tasks.ts +0 -347
- package/src/db/threads.ts +0 -276
- package/src/db/workers.ts +0 -212
- package/src/tools/context/list-drives.ts +0 -36
- package/src/tools/context/refresh.ts +0 -165
- package/src/tools/context/search.ts +0 -54
|
@@ -1,28 +1,11 @@
|
|
|
1
|
-
import { isText } from "istextorbinary";
|
|
2
1
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
4
|
-
import { ingestByPath } from "../../context/ingest.ts";
|
|
5
|
-
import {
|
|
6
|
-
createContextItemStrict,
|
|
7
|
-
PathConflictError,
|
|
8
|
-
upsertContextItem,
|
|
9
|
-
} from "../../db/context.ts";
|
|
2
|
+
import { PathConflictError, writeContextFile } from "../../context/store.ts";
|
|
10
3
|
import { getTool, type ToolDefinition } from "../tool.ts";
|
|
11
4
|
|
|
12
5
|
const PREVIEW_CHARS = 200;
|
|
13
6
|
const ERROR_MESSAGE_CAP = 2000;
|
|
14
7
|
const TOOL_NAME = "pipe_to_context";
|
|
15
8
|
|
|
16
|
-
function mimeFromPath(path: string): string {
|
|
17
|
-
const type = Bun.file(path).type.split(";")[0];
|
|
18
|
-
return type ?? "application/octet-stream";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function isTextualPath(path: string): boolean {
|
|
22
|
-
const filename = path.split("/").pop() ?? path;
|
|
23
|
-
return isText(filename) !== false;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
9
|
function truncate(s: string, cap: number): string {
|
|
27
10
|
if (s.length <= cap) return s;
|
|
28
11
|
return `${s.slice(0, cap)}…[truncated, ${s.length - cap} more chars]`;
|
|
@@ -32,39 +15,29 @@ const inputSchema = z.object({
|
|
|
32
15
|
tool_name: z
|
|
33
16
|
.string()
|
|
34
17
|
.describe(
|
|
35
|
-
"Name of the tool to dispatch. Its full output is piped into a context
|
|
18
|
+
"Name of the tool to dispatch. Its full output is piped into a file under context/; you (the LLM) will only see the storage acknowledgment, never the raw bytes.",
|
|
36
19
|
),
|
|
37
20
|
tool_input: z
|
|
38
21
|
.record(z.string(), z.unknown())
|
|
39
22
|
.describe(
|
|
40
23
|
"Arguments to pass to the inner tool (same shape as a normal call).",
|
|
41
24
|
),
|
|
42
|
-
|
|
25
|
+
path: z
|
|
43
26
|
.string()
|
|
44
|
-
.default("agent")
|
|
45
27
|
.describe(
|
|
46
|
-
"
|
|
28
|
+
"Project-relative path under context/ to write the captured output to (e.g. 'gdoc/quarterly-plan.md').",
|
|
47
29
|
),
|
|
48
|
-
path: z.string().describe("Path within the drive (starts with /)"),
|
|
49
|
-
title: z
|
|
50
|
-
.string()
|
|
51
|
-
.optional()
|
|
52
|
-
.describe("Title for the file (defaults to filename)"),
|
|
53
|
-
description: z.string().optional().describe("Description of the file"),
|
|
54
30
|
on_conflict: z
|
|
55
31
|
.enum(["error", "overwrite"])
|
|
56
32
|
.optional()
|
|
57
33
|
.describe(
|
|
58
|
-
"What to do if a file already exists at this
|
|
34
|
+
"What to do if a file already exists at this path. Defaults to 'error'. Pass 'overwrite' to replace.",
|
|
59
35
|
),
|
|
60
36
|
});
|
|
61
37
|
|
|
62
38
|
const outputSchema = z.object({
|
|
63
39
|
is_error: z.boolean(),
|
|
64
|
-
id: z.string().optional(),
|
|
65
|
-
drive: z.string().optional(),
|
|
66
40
|
path: z.string().optional(),
|
|
67
|
-
ref: z.string().optional(),
|
|
68
41
|
bytes_written: z.number().optional(),
|
|
69
42
|
preview: z
|
|
70
43
|
.string()
|
|
@@ -89,7 +62,7 @@ const outputSchema = z.object({
|
|
|
89
62
|
export const pipeToContextTool = {
|
|
90
63
|
name: TOOL_NAME,
|
|
91
64
|
description:
|
|
92
|
-
"[[ bash equivalent command: cmd > file ]] Run another tool and pipe its full output directly into a context
|
|
65
|
+
"[[ bash equivalent command: cmd > file ]] Run another tool and pipe its full output directly into a file under context/, without the result flowing through the conversation. Use this when you need a large tool output (Google Docs via mcp_exec, web fetches, search dumps) saved for later inspection but you do NOT need to read the bytes yourself. You'll only see the storage ack (path, size, short preview).",
|
|
93
66
|
group: "context",
|
|
94
67
|
inputSchema,
|
|
95
68
|
outputSchema,
|
|
@@ -111,7 +84,7 @@ export const pipeToContextTool = {
|
|
|
111
84
|
error_type: "forbidden_tool",
|
|
112
85
|
message: `Tool "${inner.name}" cannot be piped (terminal tools and pipe_to_context itself are not allowed).`,
|
|
113
86
|
next_action_hint:
|
|
114
|
-
"Pipe a non-terminal tool (
|
|
87
|
+
"Pipe a non-terminal tool (search, mcp_exec, etc.) instead.",
|
|
115
88
|
};
|
|
116
89
|
}
|
|
117
90
|
|
|
@@ -169,43 +142,16 @@ export const pipeToContextTool = {
|
|
|
169
142
|
};
|
|
170
143
|
}
|
|
171
144
|
|
|
172
|
-
const mimeType = mimeFromPath(input.path);
|
|
173
|
-
const isTextual = isTextualPath(input.path);
|
|
174
|
-
const title =
|
|
175
|
-
input.title ?? input.path.split("/").filter(Boolean).pop() ?? input.path;
|
|
176
|
-
const onConflict = input.on_conflict ?? "error";
|
|
177
|
-
const target = { drive: input.drive, path: input.path };
|
|
178
|
-
|
|
179
145
|
try {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
drive: target.drive,
|
|
187
|
-
path: target.path,
|
|
188
|
-
mimeType,
|
|
189
|
-
isTextual,
|
|
190
|
-
})
|
|
191
|
-
: await createContextItemStrict(ctx.conn, {
|
|
192
|
-
title,
|
|
193
|
-
description: input.description,
|
|
194
|
-
content: innerOutput,
|
|
195
|
-
drive: target.drive,
|
|
196
|
-
path: target.path,
|
|
197
|
-
mimeType,
|
|
198
|
-
isTextual,
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
await ingestByPath(ctx.conn, target, ctx.config);
|
|
202
|
-
|
|
146
|
+
const entry = await writeContextFile(
|
|
147
|
+
ctx.projectDir,
|
|
148
|
+
input.path,
|
|
149
|
+
innerOutput,
|
|
150
|
+
{ onConflict: input.on_conflict ?? "error" },
|
|
151
|
+
);
|
|
203
152
|
return {
|
|
204
153
|
is_error: false,
|
|
205
|
-
|
|
206
|
-
drive: item.drive,
|
|
207
|
-
path: item.path,
|
|
208
|
-
ref: formatDriveRef(item),
|
|
154
|
+
path: entry.path,
|
|
209
155
|
bytes_written: innerOutput.length,
|
|
210
156
|
preview: innerOutput.slice(0, PREVIEW_CHARS),
|
|
211
157
|
};
|
|
@@ -214,10 +160,8 @@ export const pipeToContextTool = {
|
|
|
214
160
|
return {
|
|
215
161
|
is_error: true,
|
|
216
162
|
error_type: "path_conflict",
|
|
217
|
-
drive: err.drive,
|
|
218
163
|
path: err.path,
|
|
219
|
-
|
|
220
|
-
message: `A file already exists at ${formatDriveRef({ drive: err.drive, path: err.path })} (id: ${err.existingId}). The inner tool ran but its output was discarded.`,
|
|
164
|
+
message: `A file already exists at context/${err.path}. The inner tool ran but its output was discarded.`,
|
|
221
165
|
next_action_hint:
|
|
222
166
|
"Retry with on_conflict='overwrite' to replace, or pick a different path.",
|
|
223
167
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { getPromptsDir } from "../../constants.ts";
|
|
4
4
|
import {
|
|
5
5
|
type ContextFileMeta,
|
|
6
6
|
parseContextFile,
|
|
@@ -25,12 +25,12 @@ const outputSchema = z.object({
|
|
|
25
25
|
export const updateBeliefsTool = {
|
|
26
26
|
name: "update_beliefs",
|
|
27
27
|
description:
|
|
28
|
-
"Update the agent's beliefs file (
|
|
28
|
+
"Update the agent's beliefs file (prompts/beliefs.md). Preserves frontmatter, replaces content body.",
|
|
29
29
|
group: "context",
|
|
30
30
|
inputSchema,
|
|
31
31
|
outputSchema,
|
|
32
32
|
execute: async (input, ctx) => {
|
|
33
|
-
const filePath = join(
|
|
33
|
+
const filePath = join(getPromptsDir(ctx.projectDir), "beliefs.md");
|
|
34
34
|
const file = Bun.file(filePath);
|
|
35
35
|
|
|
36
36
|
let meta: ContextFileMeta = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { getPromptsDir } from "../../constants.ts";
|
|
4
4
|
import {
|
|
5
5
|
type ContextFileMeta,
|
|
6
6
|
parseContextFile,
|
|
@@ -25,12 +25,12 @@ const outputSchema = z.object({
|
|
|
25
25
|
export const updateGoalsTool = {
|
|
26
26
|
name: "update_goals",
|
|
27
27
|
description:
|
|
28
|
-
"Update the agent's goals file (
|
|
28
|
+
"Update the agent's goals file (prompts/goals.md). Preserves frontmatter, replaces content body.",
|
|
29
29
|
group: "context",
|
|
30
30
|
inputSchema,
|
|
31
31
|
outputSchema,
|
|
32
32
|
execute: async (input, ctx) => {
|
|
33
|
-
const filePath = join(
|
|
33
|
+
const filePath = join(getPromptsDir(ctx.projectDir), "goals.md");
|
|
34
34
|
const file = Bun.file(filePath);
|
|
35
35
|
|
|
36
36
|
let meta: ContextFileMeta = {
|
package/src/tools/dir/create.ts
CHANGED
|
@@ -1,44 +1,47 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
import { contextPathExists, createContextItem } from "../../db/context.ts";
|
|
2
|
+
import { createContextDir, fileExists } from "../../context/store.ts";
|
|
4
3
|
import type { ToolDefinition } from "../tool.ts";
|
|
5
4
|
|
|
6
5
|
const inputSchema = z.object({
|
|
7
|
-
|
|
8
|
-
.string()
|
|
9
|
-
.default("agent")
|
|
10
|
-
.describe("Drive to create the directory in (defaults to 'agent')"),
|
|
11
|
-
path: z.string().describe("Directory path to create (starts with /)"),
|
|
6
|
+
path: z.string().describe("Directory path to create under context/"),
|
|
12
7
|
});
|
|
13
8
|
|
|
14
9
|
const outputSchema = z.object({
|
|
10
|
+
path: z.string(),
|
|
15
11
|
created: z.boolean(),
|
|
16
|
-
ref: z.string(),
|
|
17
12
|
is_error: z.boolean(),
|
|
13
|
+
error_type: z.string().optional(),
|
|
14
|
+
message: z.string().optional(),
|
|
15
|
+
next_action_hint: z.string().optional(),
|
|
18
16
|
});
|
|
19
17
|
|
|
20
18
|
export const contextCreateDirTool = {
|
|
21
19
|
name: "context_create_dir",
|
|
22
20
|
description:
|
|
23
|
-
"[[ bash equivalent command: mkdir -p ]] Create a directory
|
|
21
|
+
"[[ bash equivalent command: mkdir -p ]] Create a directory (recursively) under context/. Paths that traverse a user symlink fail with PathEscapeError.",
|
|
24
22
|
group: "context",
|
|
25
23
|
inputSchema,
|
|
26
24
|
outputSchema,
|
|
27
25
|
execute: async (input, ctx) => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return {
|
|
26
|
+
try {
|
|
27
|
+
const existed = await fileExists(ctx.projectDir, input.path);
|
|
28
|
+
await createContextDir(ctx.projectDir, input.path);
|
|
29
|
+
return { path: input.path, created: !existed, is_error: false };
|
|
30
|
+
} catch (err) {
|
|
31
|
+
// mkdir surfaces ENOTDIR when a path component is a file, EACCES on
|
|
32
|
+
// permission issues, etc. Convert to a structured error so the agent
|
|
33
|
+
// can pick a different parent or read what's actually there first.
|
|
34
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
35
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
36
|
+
return {
|
|
37
|
+
path: input.path,
|
|
38
|
+
created: false,
|
|
39
|
+
is_error: true,
|
|
40
|
+
error_type: code === "ENOTDIR" ? "not_a_directory" : "create_failed",
|
|
41
|
+
message,
|
|
42
|
+
next_action_hint:
|
|
43
|
+
"Run context_tree on the parent path to see what's there before retrying.",
|
|
44
|
+
};
|
|
32
45
|
}
|
|
33
|
-
|
|
34
|
-
await createContextItem(ctx.conn, {
|
|
35
|
-
title: input.path.split("/").filter(Boolean).pop() ?? input.path,
|
|
36
|
-
drive: target.drive,
|
|
37
|
-
path: target.path,
|
|
38
|
-
mimeType: "inode/directory",
|
|
39
|
-
isTextual: false,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
return { created: true, ref: formatDriveRef(target), is_error: false };
|
|
43
46
|
},
|
|
44
47
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/dir/size.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
dirSizeBytes,
|
|
4
|
+
NotDirectoryError,
|
|
5
|
+
NotFoundError,
|
|
6
|
+
} from "../../context/store.ts";
|
|
3
7
|
import type { ToolDefinition } from "../tool.ts";
|
|
4
8
|
|
|
5
9
|
function formatBytes(bytes: number): string {
|
|
@@ -11,38 +15,63 @@ function formatBytes(bytes: number): string {
|
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
const inputSchema = z.object({
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
recursive: z
|
|
17
|
-
.boolean()
|
|
18
|
+
path: z
|
|
19
|
+
.string()
|
|
18
20
|
.optional()
|
|
19
|
-
.
|
|
21
|
+
.default("")
|
|
22
|
+
.describe("Directory path under context/ (defaults to context root)"),
|
|
20
23
|
});
|
|
21
24
|
|
|
22
25
|
const outputSchema = z.object({
|
|
26
|
+
files: z.number(),
|
|
23
27
|
bytes: z.number(),
|
|
24
28
|
formatted: z.string(),
|
|
25
29
|
is_error: z.boolean(),
|
|
30
|
+
error_type: z.string().optional(),
|
|
31
|
+
message: z.string().optional(),
|
|
26
32
|
});
|
|
27
33
|
|
|
28
34
|
export const contextDirSizeTool = {
|
|
29
35
|
name: "context_dir_size",
|
|
30
36
|
description:
|
|
31
|
-
"[[ bash equivalent command: du -s ]] Get the total size of
|
|
37
|
+
"[[ bash equivalent command: du -s ]] Get the total size of files under a directory in context/.",
|
|
32
38
|
group: "context",
|
|
33
39
|
inputSchema,
|
|
34
40
|
outputSchema,
|
|
35
41
|
execute: async (input, ctx) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
try {
|
|
43
|
+
const { files, bytes } = await dirSizeBytes(
|
|
44
|
+
ctx.projectDir,
|
|
45
|
+
input.path ?? "",
|
|
46
|
+
);
|
|
47
|
+
return {
|
|
48
|
+
files,
|
|
49
|
+
bytes,
|
|
50
|
+
formatted: formatBytes(bytes),
|
|
51
|
+
is_error: false,
|
|
52
|
+
};
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (err instanceof NotFoundError) {
|
|
55
|
+
return {
|
|
56
|
+
files: 0,
|
|
57
|
+
bytes: 0,
|
|
58
|
+
formatted: formatBytes(0),
|
|
59
|
+
is_error: true,
|
|
60
|
+
error_type: "not_found",
|
|
61
|
+
message: `No directory at context/${err.path}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (err instanceof NotDirectoryError) {
|
|
65
|
+
return {
|
|
66
|
+
files: 0,
|
|
67
|
+
bytes: 0,
|
|
68
|
+
formatted: formatBytes(0),
|
|
69
|
+
is_error: true,
|
|
70
|
+
error_type: "not_a_directory",
|
|
71
|
+
message: `context/${err.path} is not a directory`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
throw err;
|
|
44
75
|
}
|
|
45
|
-
|
|
46
|
-
return { bytes, formatted: formatBytes(bytes), is_error: false };
|
|
47
76
|
},
|
|
48
77
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|