botholomew 0.12.3 → 0.13.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 +3 -3
- package/src/chat/agent.ts +42 -82
- package/src/chat/session.ts +29 -25
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +177 -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 +630 -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 +279 -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 +73 -279
- package/src/tools/file/copy.ts +50 -24
- package/src/tools/file/count-lines.ts +34 -10
- package/src/tools/file/delete.ts +44 -23
- package/src/tools/file/edit.ts +39 -14
- package/src/tools/file/exists.ts +12 -26
- package/src/tools/file/info.ts +25 -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 +3 -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/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/StatusBar.tsx +15 -15
- package/src/tui/components/TaskPanel.tsx +34 -38
- package/src/tui/components/ThreadPanel.tsx +29 -38
- package/src/tui/components/WorkerPanel.tsx +21 -19
- package/src/tui/markdown.ts +2 -8
- package/src/types/file-imports.d.ts +9 -0
- 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
package/src/tools/file/edit.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
applyPatches,
|
|
4
|
+
IsDirectoryError,
|
|
5
|
+
NotFoundError,
|
|
6
|
+
readContextFile,
|
|
7
|
+
} from "../../context/store.ts";
|
|
4
8
|
import type { ToolDefinition } from "../tool.ts";
|
|
5
9
|
|
|
6
10
|
const PatchSchema = z.object({
|
|
@@ -14,8 +18,7 @@ const PatchSchema = z.object({
|
|
|
14
18
|
});
|
|
15
19
|
|
|
16
20
|
const inputSchema = z.object({
|
|
17
|
-
|
|
18
|
-
path: z.string().describe("Path within the drive (starts with /)"),
|
|
21
|
+
path: z.string().describe("Project-relative path under context/"),
|
|
19
22
|
patches: z.array(PatchSchema).describe("Patches to apply"),
|
|
20
23
|
});
|
|
21
24
|
|
|
@@ -23,24 +26,46 @@ const outputSchema = z.object({
|
|
|
23
26
|
applied: z.number(),
|
|
24
27
|
content: z.string(),
|
|
25
28
|
is_error: z.boolean(),
|
|
29
|
+
error_type: z.string().optional(),
|
|
30
|
+
message: z.string().optional(),
|
|
26
31
|
});
|
|
27
32
|
|
|
28
33
|
export const contextEditTool = {
|
|
29
34
|
name: "context_edit",
|
|
30
35
|
description:
|
|
31
|
-
"[[ bash equivalent command: patch ]] Apply
|
|
36
|
+
"[[ bash equivalent command: patch ]] Apply line-range patches to a file under context/. Each patch specifies start_line/end_line/content.",
|
|
32
37
|
group: "context",
|
|
33
38
|
inputSchema,
|
|
34
39
|
outputSchema,
|
|
35
40
|
execute: async (input, ctx) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
try {
|
|
42
|
+
const { applied } = await applyPatches(
|
|
43
|
+
ctx.projectDir,
|
|
44
|
+
input.path,
|
|
45
|
+
input.patches,
|
|
46
|
+
);
|
|
47
|
+
const content = await readContextFile(ctx.projectDir, input.path);
|
|
48
|
+
return { applied, content, is_error: false };
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err instanceof NotFoundError) {
|
|
51
|
+
return {
|
|
52
|
+
applied: 0,
|
|
53
|
+
content: "",
|
|
54
|
+
is_error: true,
|
|
55
|
+
error_type: "not_found",
|
|
56
|
+
message: `No file at context/${err.path}`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (err instanceof IsDirectoryError) {
|
|
60
|
+
return {
|
|
61
|
+
applied: 0,
|
|
62
|
+
content: "",
|
|
63
|
+
is_error: true,
|
|
64
|
+
error_type: "is_directory",
|
|
65
|
+
message: `context/${err.path} is a directory`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
45
70
|
},
|
|
46
71
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/file/exists.ts
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { isUuid } from "../../db/uuid.ts";
|
|
2
|
+
import { fileExists } from "../../context/store.ts";
|
|
3
|
+
import { PathEscapeError } from "../../fs/sandbox.ts";
|
|
5
4
|
import type { ToolDefinition } from "../tool.ts";
|
|
6
5
|
|
|
7
6
|
const inputSchema = z.object({
|
|
8
|
-
|
|
9
|
-
.string()
|
|
10
|
-
.optional()
|
|
11
|
-
.describe("Drive name. Optional when `path` is a UUID or 'drive:/path'."),
|
|
12
|
-
path: z
|
|
13
|
-
.string()
|
|
14
|
-
.describe("Path within the drive (or UUID / drive:/path ref)"),
|
|
7
|
+
path: z.string().describe("Path under context/"),
|
|
15
8
|
});
|
|
16
9
|
|
|
17
10
|
const outputSchema = z.object({
|
|
@@ -22,26 +15,19 @@ const outputSchema = z.object({
|
|
|
22
15
|
export const contextExistsTool = {
|
|
23
16
|
name: "context_exists",
|
|
24
17
|
description:
|
|
25
|
-
"[[ bash equivalent command: test -e ]] Check
|
|
18
|
+
"[[ bash equivalent command: test -e ]] Check whether a path exists under context/.",
|
|
26
19
|
group: "context",
|
|
27
20
|
inputSchema,
|
|
28
21
|
outputSchema,
|
|
29
22
|
execute: async (input, ctx) => {
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
return { exists
|
|
23
|
+
try {
|
|
24
|
+
const exists = await fileExists(ctx.projectDir, input.path);
|
|
25
|
+
return { exists, is_error: false };
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (err instanceof PathEscapeError) {
|
|
28
|
+
return { exists: false, is_error: false };
|
|
29
|
+
}
|
|
30
|
+
throw err;
|
|
33
31
|
}
|
|
34
|
-
const parsed = parseDriveRef(input.path);
|
|
35
|
-
if (parsed) {
|
|
36
|
-
const item = await getContextItem(ctx.conn, parsed);
|
|
37
|
-
return { exists: item !== null, is_error: false };
|
|
38
|
-
}
|
|
39
|
-
if (!input.drive) return { exists: false, is_error: false };
|
|
40
|
-
const path = input.path.startsWith("/") ? input.path : `/${input.path}`;
|
|
41
|
-
const item = await getContextItem(ctx.conn, {
|
|
42
|
-
drive: input.drive,
|
|
43
|
-
path,
|
|
44
|
-
});
|
|
45
|
-
return { exists: item !== null, is_error: false };
|
|
46
32
|
},
|
|
47
33
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/file/info.ts
CHANGED
|
@@ -1,41 +1,20 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
findNearbyContextPaths,
|
|
5
|
-
getContextItem,
|
|
6
|
-
getContextItemById,
|
|
7
|
-
} from "../../db/context.ts";
|
|
8
|
-
import { isUuid } from "../../db/uuid.ts";
|
|
2
|
+
import { getInfo, readContextFile } from "../../context/store.ts";
|
|
9
3
|
import type { ToolDefinition } from "../tool.ts";
|
|
10
4
|
|
|
11
5
|
const inputSchema = z.object({
|
|
12
|
-
|
|
13
|
-
.string()
|
|
14
|
-
.optional()
|
|
15
|
-
.describe(
|
|
16
|
-
"Drive name (e.g. 'disk', 'url', 'agent'). Optional when `path` is a UUID or already in `drive:/...` form.",
|
|
17
|
-
),
|
|
18
|
-
path: z
|
|
19
|
-
.string()
|
|
20
|
-
.describe(
|
|
21
|
-
"Path within the drive (starts with /), or a bare UUID / 'drive:/path' ref.",
|
|
22
|
-
),
|
|
6
|
+
path: z.string().describe("Path under context/"),
|
|
23
7
|
});
|
|
24
8
|
|
|
25
9
|
const fileSchema = z.object({
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
description: z.string(),
|
|
29
|
-
mime_type: z.string(),
|
|
10
|
+
path: z.string(),
|
|
11
|
+
is_directory: z.boolean(),
|
|
30
12
|
is_textual: z.boolean(),
|
|
13
|
+
mime_type: z.string(),
|
|
31
14
|
size: z.number(),
|
|
32
15
|
lines: z.number(),
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
ref: z.string(),
|
|
36
|
-
indexed_at: z.string().nullable(),
|
|
37
|
-
created_at: z.string(),
|
|
38
|
-
updated_at: z.string(),
|
|
16
|
+
mtime: z.string(),
|
|
17
|
+
content_hash: z.string().nullable(),
|
|
39
18
|
});
|
|
40
19
|
|
|
41
20
|
const outputSchema = z.object({
|
|
@@ -49,74 +28,35 @@ const outputSchema = z.object({
|
|
|
49
28
|
export const contextInfoTool = {
|
|
50
29
|
name: "context_info",
|
|
51
30
|
description:
|
|
52
|
-
"[[ bash equivalent command: stat ]] Show
|
|
31
|
+
"[[ bash equivalent command: stat ]] Show metadata for a path under context/: size, MIME type, line count, mtime, content hash.",
|
|
53
32
|
group: "context",
|
|
54
33
|
inputSchema,
|
|
55
34
|
outputSchema,
|
|
56
35
|
execute: async (input, ctx) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (isUuid(input.path)) {
|
|
60
|
-
const byId = await getContextItemById(ctx.conn, input.path);
|
|
61
|
-
if (byId) {
|
|
62
|
-
drive = byId.drive;
|
|
63
|
-
path = byId.path;
|
|
64
|
-
}
|
|
65
|
-
} else {
|
|
66
|
-
const parsed = parseDriveRef(input.path);
|
|
67
|
-
if (parsed) {
|
|
68
|
-
drive = parsed.drive;
|
|
69
|
-
path = parsed.path;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (!drive) {
|
|
74
|
-
return {
|
|
75
|
-
is_error: true,
|
|
76
|
-
error_type: "missing_drive",
|
|
77
|
-
message: `Cannot resolve context item: no drive provided and \`${input.path}\` is not a UUID or \`drive:/path\` ref.`,
|
|
78
|
-
next_action_hint:
|
|
79
|
-
"Pass `drive` explicitly, or use a `drive:/path` ref. Call context_list_drives to see which drives exist.",
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (!path.startsWith("/")) path = `/${path}`;
|
|
84
|
-
|
|
85
|
-
const item = await getContextItem(ctx.conn, { drive, path });
|
|
86
|
-
if (!item) {
|
|
87
|
-
const { parent, siblings, walkedUp } = await findNearbyContextPaths(
|
|
88
|
-
ctx.conn,
|
|
89
|
-
drive,
|
|
90
|
-
path,
|
|
91
|
-
);
|
|
92
|
-
const hint =
|
|
93
|
-
siblings.length > 0
|
|
94
|
-
? `${walkedUp ? `Parent ${parent} has no direct entries; ` : ""}Nearby items under ${parent}: ${siblings.join(", ")}. Call context_tree({drive:"${drive}",path:"${parent.replace(/^[^:]*:/, "")}"}) to see more.`
|
|
95
|
-
: `No items found under ${parent}. Call context_list_drives to see which drives exist.`;
|
|
36
|
+
const info = await getInfo(ctx.projectDir, input.path);
|
|
37
|
+
if (!info) {
|
|
96
38
|
return {
|
|
97
39
|
is_error: true,
|
|
98
40
|
error_type: "not_found",
|
|
99
|
-
message: `No
|
|
100
|
-
next_action_hint:
|
|
41
|
+
message: `No path at context/${input.path}`,
|
|
42
|
+
next_action_hint: "Call context_tree to browse.",
|
|
101
43
|
};
|
|
102
44
|
}
|
|
103
|
-
|
|
104
|
-
|
|
45
|
+
let lines = 0;
|
|
46
|
+
if (info.is_textual && !info.is_directory) {
|
|
47
|
+
const content = await readContextFile(ctx.projectDir, input.path);
|
|
48
|
+
lines = content === "" ? 0 : content.split("\n").length;
|
|
49
|
+
}
|
|
105
50
|
return {
|
|
106
51
|
file: {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
mime_type:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
path: item.path,
|
|
116
|
-
ref: formatDriveRef(item),
|
|
117
|
-
indexed_at: item.indexed_at?.toISOString() ?? null,
|
|
118
|
-
created_at: item.created_at.toISOString(),
|
|
119
|
-
updated_at: item.updated_at.toISOString(),
|
|
52
|
+
path: info.path,
|
|
53
|
+
is_directory: info.is_directory,
|
|
54
|
+
is_textual: info.is_textual,
|
|
55
|
+
mime_type: info.mime_type,
|
|
56
|
+
size: info.size,
|
|
57
|
+
lines,
|
|
58
|
+
mtime: info.mtime.toISOString(),
|
|
59
|
+
content_hash: info.content_hash,
|
|
120
60
|
},
|
|
121
61
|
is_error: false,
|
|
122
62
|
};
|
package/src/tools/file/move.ts
CHANGED
|
@@ -1,46 +1,61 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { formatDriveRef } from "../../context/drives.ts";
|
|
3
2
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
deleteContextPath,
|
|
4
|
+
fileExists,
|
|
5
|
+
moveContextPath,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
PathConflictError,
|
|
8
|
+
} from "../../context/store.ts";
|
|
8
9
|
import type { ToolDefinition } from "../tool.ts";
|
|
9
10
|
|
|
10
11
|
const inputSchema = z.object({
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
dst_drive: z.string().describe("Destination drive"),
|
|
14
|
-
dst_path: z.string().describe("Destination path within the drive"),
|
|
12
|
+
src: z.string().describe("Source path under context/"),
|
|
13
|
+
dst: z.string().describe("Destination path under context/"),
|
|
15
14
|
overwrite: z.boolean().optional().describe("Overwrite if destination exists"),
|
|
16
15
|
});
|
|
17
16
|
|
|
18
17
|
const outputSchema = z.object({
|
|
19
|
-
|
|
18
|
+
src: z.string(),
|
|
19
|
+
dst: z.string(),
|
|
20
20
|
is_error: z.boolean(),
|
|
21
|
+
error_type: z.string().optional(),
|
|
22
|
+
message: z.string().optional(),
|
|
21
23
|
});
|
|
22
24
|
|
|
23
25
|
export const contextMoveTool = {
|
|
24
26
|
name: "context_move",
|
|
25
27
|
description:
|
|
26
|
-
"[[ bash equivalent command: mv ]] Move or rename a
|
|
28
|
+
"[[ bash equivalent command: mv ]] Move or rename a file/directory under context/.",
|
|
27
29
|
group: "context",
|
|
28
30
|
inputSchema,
|
|
29
31
|
outputSchema,
|
|
30
32
|
execute: async (input, ctx) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
try {
|
|
34
|
+
if (input.overwrite && (await fileExists(ctx.projectDir, input.dst))) {
|
|
35
|
+
await deleteContextPath(ctx.projectDir, input.dst, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
await moveContextPath(ctx.projectDir, input.src, input.dst);
|
|
38
|
+
return { src: input.src, dst: input.dst, is_error: false };
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err instanceof NotFoundError) {
|
|
41
|
+
return {
|
|
42
|
+
src: input.src,
|
|
43
|
+
dst: input.dst,
|
|
44
|
+
is_error: true,
|
|
45
|
+
error_type: "not_found",
|
|
46
|
+
message: `No file at context/${err.path}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (err instanceof PathConflictError) {
|
|
50
|
+
return {
|
|
51
|
+
src: input.src,
|
|
52
|
+
dst: input.dst,
|
|
53
|
+
is_error: true,
|
|
54
|
+
error_type: "path_conflict",
|
|
55
|
+
message: `Destination already exists at context/${err.path}; pass overwrite=true to replace.`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
throw err;
|
|
40
59
|
}
|
|
41
|
-
|
|
42
|
-
await moveContextItem(ctx.conn, src, dst);
|
|
43
|
-
|
|
44
|
-
return { ref: formatDriveRef(dst), is_error: false };
|
|
45
60
|
},
|
|
46
61
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/file/read.ts
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { formatDriveRef, parseDriveRef } from "../../context/drives.ts";
|
|
3
2
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "../../
|
|
8
|
-
import { isUuid } from "../../db/uuid.ts";
|
|
3
|
+
IsDirectoryError,
|
|
4
|
+
NotFoundError,
|
|
5
|
+
readContextFile,
|
|
6
|
+
} from "../../context/store.ts";
|
|
9
7
|
import type { ToolDefinition } from "../tool.ts";
|
|
10
8
|
|
|
11
9
|
const inputSchema = z.object({
|
|
12
|
-
drive: z
|
|
13
|
-
.string()
|
|
14
|
-
.optional()
|
|
15
|
-
.describe(
|
|
16
|
-
"Drive name (e.g. 'disk', 'url', 'agent', 'google-docs', 'github'). Use context_list_drives to see what's available. Optional when `path` is a UUID or already in `drive:/...` form.",
|
|
17
|
-
),
|
|
18
10
|
path: z
|
|
19
11
|
.string()
|
|
20
12
|
.describe(
|
|
21
|
-
"
|
|
13
|
+
"Project-relative path under context/ (e.g. 'notes/foo.md'). Forward-slashes; never absolute.",
|
|
22
14
|
),
|
|
23
15
|
offset: z
|
|
24
16
|
.number()
|
|
@@ -37,79 +29,39 @@ const outputSchema = z.object({
|
|
|
37
29
|
|
|
38
30
|
export const contextReadTool = {
|
|
39
31
|
name: "context_read",
|
|
40
|
-
description:
|
|
41
|
-
"[[ bash equivalent command: cat ]] Read a context item's contents.",
|
|
32
|
+
description: "[[ bash equivalent command: cat ]] Read a file under context/.",
|
|
42
33
|
group: "context",
|
|
43
34
|
inputSchema,
|
|
44
35
|
outputSchema,
|
|
45
36
|
execute: async (input, ctx) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
path = byId.path;
|
|
37
|
+
try {
|
|
38
|
+
let content = await readContextFile(ctx.projectDir, input.path);
|
|
39
|
+
if (input.offset || input.limit) {
|
|
40
|
+
const lines = content.split("\n");
|
|
41
|
+
const start = (input.offset ?? 1) - 1;
|
|
42
|
+
const end = input.limit ? start + input.limit : lines.length;
|
|
43
|
+
content = lines.slice(start, end).join("\n");
|
|
54
44
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
return { content, is_error: false };
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (err instanceof NotFoundError) {
|
|
48
|
+
return {
|
|
49
|
+
is_error: true,
|
|
50
|
+
error_type: "not_found",
|
|
51
|
+
message: `No file at context/${err.path}`,
|
|
52
|
+
next_action_hint:
|
|
53
|
+
"Call context_tree to browse, or context_exists to check first.",
|
|
54
|
+
};
|
|
60
55
|
}
|
|
56
|
+
if (err instanceof IsDirectoryError) {
|
|
57
|
+
return {
|
|
58
|
+
is_error: true,
|
|
59
|
+
error_type: "is_directory",
|
|
60
|
+
message: `context/${err.path} is a directory`,
|
|
61
|
+
next_action_hint: "Use context_tree to list its contents.",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
throw err;
|
|
61
65
|
}
|
|
62
|
-
|
|
63
|
-
if (!drive) {
|
|
64
|
-
return {
|
|
65
|
-
is_error: true,
|
|
66
|
-
error_type: "missing_drive",
|
|
67
|
-
message: `Cannot resolve context item: no drive provided and \`${input.path}\` is not a UUID or \`drive:/path\` ref.`,
|
|
68
|
-
next_action_hint:
|
|
69
|
-
"Pass `drive` explicitly, or use a `drive:/path` ref. Call context_list_drives to see which drives exist.",
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (!path.startsWith("/")) path = `/${path}`;
|
|
74
|
-
|
|
75
|
-
const item = await getContextItem(ctx.conn, { drive, path });
|
|
76
|
-
if (!item) {
|
|
77
|
-
const { parent, siblings, walkedUp } = await findNearbyContextPaths(
|
|
78
|
-
ctx.conn,
|
|
79
|
-
drive,
|
|
80
|
-
path,
|
|
81
|
-
);
|
|
82
|
-
const hint =
|
|
83
|
-
siblings.length > 0
|
|
84
|
-
? `${walkedUp ? `Parent ${parent} has no direct entries; ` : ""}Nearby items under ${parent}: ${siblings.join(", ")}. Call context_tree({drive:"${drive}",path:"${parent.replace(/^[^:]*:/, "")}"}) to see more.`
|
|
85
|
-
: `No items found under ${parent}. Call context_list_drives to see which drives exist.`;
|
|
86
|
-
return {
|
|
87
|
-
is_error: true,
|
|
88
|
-
error_type: "not_found",
|
|
89
|
-
message: `No context item at ${formatDriveRef({ drive, path })}`,
|
|
90
|
-
next_action_hint: hint,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (item.content == null) {
|
|
95
|
-
return {
|
|
96
|
-
is_error: true,
|
|
97
|
-
error_type: "no_text_content",
|
|
98
|
-
message: `Context item ${formatDriveRef(item)} has no text content (mime: ${item.mime_type})`,
|
|
99
|
-
next_action_hint:
|
|
100
|
-
"Binary items can't be read as text. Call context_info to inspect metadata, or pick a textual sibling.",
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
let content = item.content;
|
|
105
|
-
|
|
106
|
-
if (input.offset || input.limit) {
|
|
107
|
-
const lines = content.split("\n");
|
|
108
|
-
const start = (input.offset ?? 1) - 1;
|
|
109
|
-
const end = input.limit ? start + input.limit : lines.length;
|
|
110
|
-
content = lines.slice(start, end).join("\n");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return { content, is_error: false };
|
|
114
66
|
},
|
|
115
67
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/file/write.ts
CHANGED
|
@@ -1,132 +1,55 @@
|
|
|
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";
|
|
10
|
-
import { buildContextTree } from "../dir/tree.ts";
|
|
2
|
+
import { PathConflictError, writeContextFile } from "../../context/store.ts";
|
|
11
3
|
import type { ToolDefinition } from "../tool.ts";
|
|
12
4
|
|
|
13
|
-
function mimeFromPath(path: string): string {
|
|
14
|
-
const type = Bun.file(path).type.split(";")[0];
|
|
15
|
-
return type ?? "application/octet-stream";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function isTextualPath(path: string): boolean {
|
|
19
|
-
const filename = path.split("/").pop() ?? path;
|
|
20
|
-
const result = isText(filename);
|
|
21
|
-
return result !== false;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
5
|
const inputSchema = z.object({
|
|
25
|
-
|
|
6
|
+
path: z
|
|
26
7
|
.string()
|
|
27
|
-
.default("agent")
|
|
28
8
|
.describe(
|
|
29
|
-
"
|
|
9
|
+
"Project-relative path under context/ (e.g. 'notes/foo.md'). Created if its parent directory does not exist.",
|
|
30
10
|
),
|
|
31
|
-
path: z.string().describe("Path within the drive (starts with /)"),
|
|
32
11
|
content: z.string().describe("Text content to write"),
|
|
33
|
-
content_base64: z
|
|
34
|
-
.string()
|
|
35
|
-
.optional()
|
|
36
|
-
.describe(
|
|
37
|
-
"Base64-encoded binary content (used instead of content for binary files)",
|
|
38
|
-
),
|
|
39
|
-
title: z
|
|
40
|
-
.string()
|
|
41
|
-
.optional()
|
|
42
|
-
.describe("Title for the file (defaults to filename)"),
|
|
43
|
-
description: z.string().optional().describe("Description of the file"),
|
|
44
12
|
on_conflict: z
|
|
45
13
|
.enum(["error", "overwrite"])
|
|
46
14
|
.optional()
|
|
47
15
|
.describe(
|
|
48
|
-
"What to do if
|
|
16
|
+
"What to do if the file already exists. Defaults to 'error'. Pass 'overwrite' to replace.",
|
|
49
17
|
),
|
|
50
18
|
});
|
|
51
19
|
|
|
52
20
|
const outputSchema = z.object({
|
|
53
|
-
id: z.string().nullable(),
|
|
54
|
-
drive: z.string(),
|
|
55
21
|
path: z.string(),
|
|
56
|
-
ref: z.string(),
|
|
57
22
|
is_error: z.boolean(),
|
|
58
23
|
error_type: z.string().optional(),
|
|
59
24
|
message: z.string().optional(),
|
|
60
25
|
next_action_hint: z.string().optional(),
|
|
61
|
-
tree: z
|
|
62
|
-
.string()
|
|
63
|
-
.optional()
|
|
64
|
-
.describe(
|
|
65
|
-
"Snapshot of the drive's tree after the write so you can see the surrounding files.",
|
|
66
|
-
),
|
|
67
26
|
});
|
|
68
27
|
|
|
69
28
|
export const contextWriteTool = {
|
|
70
29
|
name: "context_write",
|
|
71
30
|
description:
|
|
72
|
-
"[[ bash equivalent command: tee ]] Write content to a
|
|
31
|
+
"[[ bash equivalent command: tee ]] Write text content to a file under context/. Fails if the path already exists unless on_conflict='overwrite'.",
|
|
73
32
|
group: "context",
|
|
74
33
|
inputSchema,
|
|
75
34
|
outputSchema,
|
|
76
35
|
execute: async (input, ctx) => {
|
|
77
|
-
const mimeType = mimeFromPath(input.path);
|
|
78
|
-
const isTextual = isTextualPath(input.path);
|
|
79
|
-
const title =
|
|
80
|
-
input.title ?? input.path.split("/").filter(Boolean).pop() ?? input.path;
|
|
81
|
-
const onConflict = input.on_conflict ?? "error";
|
|
82
|
-
const target = { drive: input.drive, path: input.path };
|
|
83
|
-
|
|
84
36
|
try {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
path: target.path,
|
|
93
|
-
mimeType,
|
|
94
|
-
isTextual,
|
|
95
|
-
})
|
|
96
|
-
: await createContextItemStrict(ctx.conn, {
|
|
97
|
-
title,
|
|
98
|
-
description: input.description,
|
|
99
|
-
content: input.content_base64 ?? input.content,
|
|
100
|
-
drive: target.drive,
|
|
101
|
-
path: target.path,
|
|
102
|
-
mimeType,
|
|
103
|
-
isTextual,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
await ingestByPath(ctx.conn, target, ctx.config);
|
|
107
|
-
const { tree } = await buildContextTree(ctx.conn, {
|
|
108
|
-
drive: target.drive,
|
|
109
|
-
});
|
|
110
|
-
return {
|
|
111
|
-
id: item.id,
|
|
112
|
-
drive: item.drive,
|
|
113
|
-
path: item.path,
|
|
114
|
-
ref: formatDriveRef(item),
|
|
115
|
-
is_error: false,
|
|
116
|
-
tree,
|
|
117
|
-
};
|
|
37
|
+
const entry = await writeContextFile(
|
|
38
|
+
ctx.projectDir,
|
|
39
|
+
input.path,
|
|
40
|
+
input.content,
|
|
41
|
+
{ onConflict: input.on_conflict ?? "error" },
|
|
42
|
+
);
|
|
43
|
+
return { path: entry.path, is_error: false };
|
|
118
44
|
} catch (err) {
|
|
119
45
|
if (err instanceof PathConflictError) {
|
|
120
46
|
return {
|
|
121
|
-
id: null,
|
|
122
|
-
drive: err.drive,
|
|
123
47
|
path: err.path,
|
|
124
|
-
ref: formatDriveRef({ drive: err.drive, path: err.path }),
|
|
125
48
|
is_error: true,
|
|
126
49
|
error_type: "path_conflict",
|
|
127
|
-
message: `A file already exists at
|
|
50
|
+
message: `A file already exists at context/${err.path}.`,
|
|
128
51
|
next_action_hint:
|
|
129
|
-
"Call context_read to inspect
|
|
52
|
+
"Call context_read to inspect, or retry with on_conflict='overwrite'.",
|
|
130
53
|
};
|
|
131
54
|
}
|
|
132
55
|
throw err;
|