botholomew 0.8.10 → 0.9.5
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/chat/agent.ts +7 -3
- package/src/commands/context.ts +223 -373
- package/src/commands/tools.ts +100 -11
- package/src/context/describer.ts +3 -118
- package/src/context/drives.ts +110 -0
- package/src/context/fetcher.ts +11 -1
- package/src/context/ingest.ts +13 -10
- package/src/context/refresh.ts +39 -24
- package/src/context/url-utils.ts +0 -23
- package/src/db/context.ts +195 -119
- package/src/db/embeddings.ts +35 -16
- package/src/db/sql/13-drive-paths.sql +49 -0
- package/src/tools/context/list-drives.ts +36 -0
- package/src/tools/context/refresh.ts +41 -23
- package/src/tools/context/search.ts +8 -3
- package/src/tools/dir/create.ts +14 -11
- package/src/tools/dir/size.ts +3 -2
- package/src/tools/dir/tree.ts +57 -17
- package/src/tools/file/copy.ts +14 -8
- package/src/tools/file/count-lines.ts +6 -3
- package/src/tools/file/delete.ts +12 -5
- package/src/tools/file/edit.ts +5 -3
- package/src/tools/file/exists.ts +25 -3
- package/src/tools/file/info.ts +90 -18
- package/src/tools/file/move.ts +15 -16
- package/src/tools/file/read.ts +79 -5
- package/src/tools/file/write.ts +29 -12
- package/src/tools/registry.ts +2 -2
- package/src/tools/search/grep.ts +44 -11
- package/src/tools/search/semantic.ts +7 -3
- package/src/tui/components/ContextPanel.tsx +73 -35
- package/src/tui/markdown.ts +2 -3
- package/src/worker/prompt.ts +5 -2
- package/src/tools/dir/list.ts +0 -89
package/src/tools/file/copy.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatDriveRef } from "../../context/drives.ts";
|
|
2
3
|
import {
|
|
3
4
|
contextPathExists,
|
|
4
5
|
copyContextItem,
|
|
@@ -7,14 +8,16 @@ import {
|
|
|
7
8
|
import type { ToolDefinition } from "../tool.ts";
|
|
8
9
|
|
|
9
10
|
const inputSchema = z.object({
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
src_drive: z.string().describe("Source drive"),
|
|
12
|
+
src_path: z.string().describe("Source path within the drive"),
|
|
13
|
+
dst_drive: z.string().describe("Destination drive"),
|
|
14
|
+
dst_path: z.string().describe("Destination path within the drive"),
|
|
12
15
|
overwrite: z.boolean().optional().describe("Overwrite if destination exists"),
|
|
13
16
|
});
|
|
14
17
|
|
|
15
18
|
const outputSchema = z.object({
|
|
16
19
|
id: z.string(),
|
|
17
|
-
|
|
20
|
+
ref: z.string(),
|
|
18
21
|
is_error: z.boolean(),
|
|
19
22
|
});
|
|
20
23
|
|
|
@@ -25,15 +28,18 @@ export const contextCopyTool = {
|
|
|
25
28
|
inputSchema,
|
|
26
29
|
outputSchema,
|
|
27
30
|
execute: async (input, ctx) => {
|
|
28
|
-
const
|
|
31
|
+
const src = { drive: input.src_drive, path: input.src_path };
|
|
32
|
+
const dst = { drive: input.dst_drive, path: input.dst_path };
|
|
33
|
+
|
|
34
|
+
const dstExists = await contextPathExists(ctx.conn, dst);
|
|
29
35
|
if (dstExists && !input.overwrite) {
|
|
30
|
-
throw new Error(`Destination already exists: ${
|
|
36
|
+
throw new Error(`Destination already exists: ${formatDriveRef(dst)}`);
|
|
31
37
|
}
|
|
32
38
|
if (dstExists) {
|
|
33
|
-
await deleteContextItemByPath(ctx.conn,
|
|
39
|
+
await deleteContextItemByPath(ctx.conn, dst);
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
const item = await copyContextItem(ctx.conn,
|
|
37
|
-
return { id: item.id,
|
|
42
|
+
const item = await copyContextItem(ctx.conn, src, dst);
|
|
43
|
+
return { id: item.id, ref: formatDriveRef(item), is_error: false };
|
|
38
44
|
},
|
|
39
45
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatDriveRef } from "../../context/drives.ts";
|
|
2
3
|
import { resolveContextItemOrThrow } from "../../db/context.ts";
|
|
3
4
|
import type { ToolDefinition } from "../tool.ts";
|
|
4
5
|
|
|
5
6
|
const inputSchema = z.object({
|
|
6
|
-
|
|
7
|
+
drive: z.string().describe("Drive name (e.g. 'disk', 'agent')"),
|
|
8
|
+
path: z.string().describe("Path within the drive"),
|
|
7
9
|
});
|
|
8
10
|
|
|
9
11
|
const outputSchema = z.object({
|
|
@@ -19,8 +21,9 @@ export const contextCountLinesTool = {
|
|
|
19
21
|
inputSchema,
|
|
20
22
|
outputSchema,
|
|
21
23
|
execute: async (input, ctx) => {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
+
const ref = formatDriveRef({ drive: input.drive, path: input.path });
|
|
25
|
+
const item = await resolveContextItemOrThrow(ctx.conn, ref);
|
|
26
|
+
if (item.content == null) throw new Error(`No text content: ${ref}`);
|
|
24
27
|
|
|
25
28
|
return { lines: item.content.split("\n").length, is_error: false };
|
|
26
29
|
},
|
package/src/tools/file/delete.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatDriveRef } from "../../context/drives.ts";
|
|
2
3
|
import {
|
|
3
4
|
deleteContextItemByPath,
|
|
4
5
|
deleteContextItemsByPrefix,
|
|
@@ -6,7 +7,8 @@ import {
|
|
|
6
7
|
import type { ToolDefinition } from "../tool.ts";
|
|
7
8
|
|
|
8
9
|
const inputSchema = z.object({
|
|
9
|
-
|
|
10
|
+
drive: z.string().describe("Drive name"),
|
|
11
|
+
path: z.string().describe("Path to delete within the drive"),
|
|
10
12
|
recursive: z
|
|
11
13
|
.boolean()
|
|
12
14
|
.optional()
|
|
@@ -30,15 +32,20 @@ export const contextDeleteTool = {
|
|
|
30
32
|
inputSchema,
|
|
31
33
|
outputSchema,
|
|
32
34
|
execute: async (input, ctx) => {
|
|
35
|
+
const target = { drive: input.drive, path: input.path };
|
|
33
36
|
if (input.recursive) {
|
|
34
|
-
const count = await deleteContextItemsByPrefix(
|
|
35
|
-
|
|
37
|
+
const count = await deleteContextItemsByPrefix(
|
|
38
|
+
ctx.conn,
|
|
39
|
+
target.drive,
|
|
40
|
+
target.path,
|
|
41
|
+
);
|
|
42
|
+
const exact = await deleteContextItemByPath(ctx.conn, target);
|
|
36
43
|
return { deleted: count + (exact ? 1 : 0), is_error: false };
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
const deleted = await deleteContextItemByPath(ctx.conn,
|
|
46
|
+
const deleted = await deleteContextItemByPath(ctx.conn, target);
|
|
40
47
|
if (!deleted && !input.force) {
|
|
41
|
-
throw new Error(`Not found: ${
|
|
48
|
+
throw new Error(`Not found: ${formatDriveRef(target)}`);
|
|
42
49
|
}
|
|
43
50
|
return { deleted: deleted ? 1 : 0, is_error: false };
|
|
44
51
|
},
|
package/src/tools/file/edit.ts
CHANGED
|
@@ -14,7 +14,8 @@ const PatchSchema = z.object({
|
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
const inputSchema = z.object({
|
|
17
|
-
|
|
17
|
+
drive: z.string().describe("Drive name (e.g. 'agent', 'disk')"),
|
|
18
|
+
path: z.string().describe("Path within the drive (starts with /)"),
|
|
18
19
|
patches: z.array(PatchSchema).describe("Patches to apply"),
|
|
19
20
|
});
|
|
20
21
|
|
|
@@ -32,13 +33,14 @@ export const contextEditTool = {
|
|
|
32
33
|
inputSchema,
|
|
33
34
|
outputSchema,
|
|
34
35
|
execute: async (input, ctx) => {
|
|
36
|
+
const target = { drive: input.drive, path: input.path };
|
|
35
37
|
const { item, applied } = await applyPatchesToContextItem(
|
|
36
38
|
ctx.conn,
|
|
37
|
-
|
|
39
|
+
target,
|
|
38
40
|
input.patches,
|
|
39
41
|
);
|
|
40
42
|
|
|
41
|
-
await ingestByPath(ctx.conn,
|
|
43
|
+
await ingestByPath(ctx.conn, target, ctx.config);
|
|
42
44
|
return { applied, content: item.content ?? "", is_error: false };
|
|
43
45
|
},
|
|
44
46
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/file/exists.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { parseDriveRef } from "../../context/drives.ts";
|
|
3
|
+
import { getContextItem, getContextItemById } from "../../db/context.ts";
|
|
4
|
+
import { isUuid } from "../../db/uuid.ts";
|
|
3
5
|
import type { ToolDefinition } from "../tool.ts";
|
|
4
6
|
|
|
5
7
|
const inputSchema = z.object({
|
|
6
|
-
|
|
8
|
+
drive: z
|
|
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
15
|
});
|
|
8
16
|
|
|
9
17
|
const outputSchema = z.object({
|
|
@@ -19,7 +27,21 @@ export const contextExistsTool = {
|
|
|
19
27
|
inputSchema,
|
|
20
28
|
outputSchema,
|
|
21
29
|
execute: async (input, ctx) => {
|
|
22
|
-
|
|
30
|
+
if (isUuid(input.path)) {
|
|
31
|
+
const item = await getContextItemById(ctx.conn, input.path);
|
|
32
|
+
return { exists: item !== null, is_error: false };
|
|
33
|
+
}
|
|
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
|
+
});
|
|
23
45
|
return { exists: item !== null, is_error: false };
|
|
24
46
|
},
|
|
25
47
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/file/info.ts
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { formatDriveRef, parseDriveRef } from "../../context/drives.ts";
|
|
3
|
+
import {
|
|
4
|
+
findNearbyContextPaths,
|
|
5
|
+
getContextItem,
|
|
6
|
+
getContextItemById,
|
|
7
|
+
} from "../../db/context.ts";
|
|
8
|
+
import { isUuid } from "../../db/uuid.ts";
|
|
3
9
|
import type { ToolDefinition } from "../tool.ts";
|
|
4
10
|
|
|
5
11
|
const inputSchema = z.object({
|
|
6
|
-
|
|
12
|
+
drive: z
|
|
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
|
+
),
|
|
7
23
|
});
|
|
8
24
|
|
|
9
|
-
const
|
|
25
|
+
const fileSchema = z.object({
|
|
10
26
|
id: z.string(),
|
|
11
27
|
title: z.string(),
|
|
12
28
|
description: z.string(),
|
|
@@ -14,12 +30,20 @@ const outputSchema = z.object({
|
|
|
14
30
|
is_textual: z.boolean(),
|
|
15
31
|
size: z.number(),
|
|
16
32
|
lines: z.number(),
|
|
17
|
-
|
|
18
|
-
|
|
33
|
+
drive: z.string(),
|
|
34
|
+
path: z.string(),
|
|
35
|
+
ref: z.string(),
|
|
19
36
|
indexed_at: z.string().nullable(),
|
|
20
37
|
created_at: z.string(),
|
|
21
38
|
updated_at: z.string(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const outputSchema = z.object({
|
|
42
|
+
file: fileSchema.optional(),
|
|
22
43
|
is_error: z.boolean(),
|
|
44
|
+
error_type: z.string().optional(),
|
|
45
|
+
message: z.string().optional(),
|
|
46
|
+
next_action_hint: z.string().optional(),
|
|
23
47
|
});
|
|
24
48
|
|
|
25
49
|
export const contextInfoTool = {
|
|
@@ -30,22 +54,70 @@ export const contextInfoTool = {
|
|
|
30
54
|
inputSchema,
|
|
31
55
|
outputSchema,
|
|
32
56
|
execute: async (input, ctx) => {
|
|
33
|
-
|
|
57
|
+
let drive = input.drive;
|
|
58
|
+
let path = input.path;
|
|
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.`;
|
|
96
|
+
return {
|
|
97
|
+
is_error: true,
|
|
98
|
+
error_type: "not_found",
|
|
99
|
+
message: `No context item at ${formatDriveRef({ drive, path })}`,
|
|
100
|
+
next_action_hint: hint,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
34
103
|
|
|
35
104
|
const content = item.content ?? "";
|
|
36
105
|
return {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
106
|
+
file: {
|
|
107
|
+
id: item.id,
|
|
108
|
+
title: item.title,
|
|
109
|
+
description: item.description,
|
|
110
|
+
mime_type: item.mime_type,
|
|
111
|
+
is_textual: item.is_textual,
|
|
112
|
+
size: content.length,
|
|
113
|
+
lines: content ? content.split("\n").length : 0,
|
|
114
|
+
drive: item.drive,
|
|
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(),
|
|
120
|
+
},
|
|
49
121
|
is_error: false,
|
|
50
122
|
};
|
|
51
123
|
},
|
package/src/tools/file/move.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatDriveRef } from "../../context/drives.ts";
|
|
2
3
|
import {
|
|
3
4
|
contextPathExists,
|
|
4
5
|
deleteContextItemByPath,
|
|
@@ -7,41 +8,39 @@ import {
|
|
|
7
8
|
import type { ToolDefinition } from "../tool.ts";
|
|
8
9
|
|
|
9
10
|
const inputSchema = z.object({
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
src_drive: z.string().describe("Source drive"),
|
|
12
|
+
src_path: z.string().describe("Source path within the drive"),
|
|
13
|
+
dst_drive: z.string().describe("Destination drive"),
|
|
14
|
+
dst_path: z.string().describe("Destination path within the drive"),
|
|
12
15
|
overwrite: z.boolean().optional().describe("Overwrite if destination exists"),
|
|
13
16
|
});
|
|
14
17
|
|
|
15
18
|
const outputSchema = z.object({
|
|
16
|
-
|
|
19
|
+
ref: z.string(),
|
|
17
20
|
is_error: z.boolean(),
|
|
18
21
|
});
|
|
19
22
|
|
|
20
23
|
export const contextMoveTool = {
|
|
21
24
|
name: "context_move",
|
|
22
25
|
description:
|
|
23
|
-
"[[ bash equivalent command: mv ]] Move or rename a context item.",
|
|
26
|
+
"[[ bash equivalent command: mv ]] Move or rename a context item (can also relocate between drives).",
|
|
24
27
|
group: "context",
|
|
25
28
|
inputSchema,
|
|
26
29
|
outputSchema,
|
|
27
30
|
execute: async (input, ctx) => {
|
|
28
|
-
const
|
|
31
|
+
const src = { drive: input.src_drive, path: input.src_path };
|
|
32
|
+
const dst = { drive: input.dst_drive, path: input.dst_path };
|
|
33
|
+
|
|
34
|
+
const dstExists = await contextPathExists(ctx.conn, dst);
|
|
29
35
|
if (dstExists && !input.overwrite) {
|
|
30
|
-
throw new Error(`Destination already exists: ${
|
|
36
|
+
throw new Error(`Destination already exists: ${formatDriveRef(dst)}`);
|
|
31
37
|
}
|
|
32
38
|
if (dstExists) {
|
|
33
|
-
await deleteContextItemByPath(ctx.conn,
|
|
39
|
+
await deleteContextItemByPath(ctx.conn, dst);
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
await moveContextItem(ctx.conn,
|
|
37
|
-
|
|
38
|
-
// Update embedding source_paths to match new location
|
|
39
|
-
await ctx.conn.queryRun(
|
|
40
|
-
"UPDATE embeddings SET source_path = ?1 WHERE source_path = ?2",
|
|
41
|
-
input.dst,
|
|
42
|
-
input.src,
|
|
43
|
-
);
|
|
42
|
+
await moveContextItem(ctx.conn, src, dst);
|
|
44
43
|
|
|
45
|
-
return {
|
|
44
|
+
return { ref: formatDriveRef(dst), is_error: false };
|
|
46
45
|
},
|
|
47
46
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/file/read.ts
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { formatDriveRef, parseDriveRef } from "../../context/drives.ts";
|
|
3
|
+
import {
|
|
4
|
+
findNearbyContextPaths,
|
|
5
|
+
getContextItem,
|
|
6
|
+
getContextItemById,
|
|
7
|
+
} from "../../db/context.ts";
|
|
8
|
+
import { isUuid } from "../../db/uuid.ts";
|
|
3
9
|
import type { ToolDefinition } from "../tool.ts";
|
|
4
10
|
|
|
5
11
|
const inputSchema = z.object({
|
|
6
|
-
|
|
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
|
+
path: z
|
|
19
|
+
.string()
|
|
20
|
+
.describe(
|
|
21
|
+
"Path within the drive (starts with /), or a bare UUID / 'drive:/path' ref (in which case `drive` is ignored).",
|
|
22
|
+
),
|
|
7
23
|
offset: z
|
|
8
24
|
.number()
|
|
9
25
|
.optional()
|
|
@@ -12,8 +28,11 @@ const inputSchema = z.object({
|
|
|
12
28
|
});
|
|
13
29
|
|
|
14
30
|
const outputSchema = z.object({
|
|
15
|
-
content: z.string(),
|
|
31
|
+
content: z.string().optional(),
|
|
16
32
|
is_error: z.boolean(),
|
|
33
|
+
error_type: z.string().optional(),
|
|
34
|
+
message: z.string().optional(),
|
|
35
|
+
next_action_hint: z.string().optional(),
|
|
17
36
|
});
|
|
18
37
|
|
|
19
38
|
export const contextReadTool = {
|
|
@@ -24,8 +43,63 @@ export const contextReadTool = {
|
|
|
24
43
|
inputSchema,
|
|
25
44
|
outputSchema,
|
|
26
45
|
execute: async (input, ctx) => {
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
// Accept either (drive, path) or a bare UUID / drive:/path ref in `path`.
|
|
47
|
+
let drive = input.drive;
|
|
48
|
+
let path = input.path;
|
|
49
|
+
if (isUuid(input.path)) {
|
|
50
|
+
const byId = await getContextItemById(ctx.conn, input.path);
|
|
51
|
+
if (byId) {
|
|
52
|
+
drive = byId.drive;
|
|
53
|
+
path = byId.path;
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
const parsed = parseDriveRef(input.path);
|
|
57
|
+
if (parsed) {
|
|
58
|
+
drive = parsed.drive;
|
|
59
|
+
path = parsed.path;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
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
|
+
}
|
|
29
103
|
|
|
30
104
|
let content = item.content;
|
|
31
105
|
|
package/src/tools/file/write.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isText } from "istextorbinary";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { formatDriveRef } from "../../context/drives.ts";
|
|
3
4
|
import { ingestByPath } from "../../context/ingest.ts";
|
|
4
5
|
import {
|
|
5
6
|
createContextItemStrict,
|
|
@@ -17,12 +18,17 @@ function mimeFromPath(path: string): string {
|
|
|
17
18
|
function isTextualPath(path: string): boolean {
|
|
18
19
|
const filename = path.split("/").pop() ?? path;
|
|
19
20
|
const result = isText(filename);
|
|
20
|
-
// isText returns null if it can't determine from filename alone — default to true
|
|
21
21
|
return result !== false;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const inputSchema = z.object({
|
|
25
|
-
|
|
25
|
+
drive: z
|
|
26
|
+
.string()
|
|
27
|
+
.default("agent")
|
|
28
|
+
.describe(
|
|
29
|
+
"Drive to write to (defaults to 'agent', which is the agent's scratch drive). Only 'agent' and drives mirroring an external system you can write back to make sense here.",
|
|
30
|
+
),
|
|
31
|
+
path: z.string().describe("Path within the drive (starts with /)"),
|
|
26
32
|
content: z.string().describe("Text content to write"),
|
|
27
33
|
content_base64: z
|
|
28
34
|
.string()
|
|
@@ -39,13 +45,15 @@ const inputSchema = z.object({
|
|
|
39
45
|
.enum(["error", "overwrite"])
|
|
40
46
|
.optional()
|
|
41
47
|
.describe(
|
|
42
|
-
"What to do if a file already exists at this path. Defaults to 'error'. Pass 'overwrite' to replace.",
|
|
48
|
+
"What to do if a file already exists at this (drive, path). Defaults to 'error'. Pass 'overwrite' to replace.",
|
|
43
49
|
),
|
|
44
50
|
});
|
|
45
51
|
|
|
46
52
|
const outputSchema = z.object({
|
|
47
53
|
id: z.string().nullable(),
|
|
54
|
+
drive: z.string(),
|
|
48
55
|
path: z.string(),
|
|
56
|
+
ref: z.string(),
|
|
49
57
|
is_error: z.boolean(),
|
|
50
58
|
error_type: z.string().optional(),
|
|
51
59
|
message: z.string().optional(),
|
|
@@ -54,14 +62,14 @@ const outputSchema = z.object({
|
|
|
54
62
|
.string()
|
|
55
63
|
.optional()
|
|
56
64
|
.describe(
|
|
57
|
-
"Snapshot of the
|
|
65
|
+
"Snapshot of the drive's tree after the write so you can see the surrounding files.",
|
|
58
66
|
),
|
|
59
67
|
});
|
|
60
68
|
|
|
61
69
|
export const contextWriteTool = {
|
|
62
70
|
name: "context_write",
|
|
63
71
|
description:
|
|
64
|
-
"[[ bash equivalent command: tee ]] Write content to a context item. By default
|
|
72
|
+
"[[ bash equivalent command: tee ]] Write content to a context item. By default writes to drive='agent' (the agent's scratch drive). Fails if the (drive, path) already exists — pass on_conflict='overwrite' to replace.",
|
|
65
73
|
group: "context",
|
|
66
74
|
inputSchema,
|
|
67
75
|
outputSchema,
|
|
@@ -71,6 +79,7 @@ export const contextWriteTool = {
|
|
|
71
79
|
const title =
|
|
72
80
|
input.title ?? input.path.split("/").filter(Boolean).pop() ?? input.path;
|
|
73
81
|
const onConflict = input.on_conflict ?? "error";
|
|
82
|
+
const target = { drive: input.drive, path: input.path };
|
|
74
83
|
|
|
75
84
|
try {
|
|
76
85
|
const item =
|
|
@@ -79,7 +88,8 @@ export const contextWriteTool = {
|
|
|
79
88
|
title,
|
|
80
89
|
description: input.description,
|
|
81
90
|
content: input.content_base64 ?? input.content,
|
|
82
|
-
|
|
91
|
+
drive: target.drive,
|
|
92
|
+
path: target.path,
|
|
83
93
|
mimeType,
|
|
84
94
|
isTextual,
|
|
85
95
|
})
|
|
@@ -87,16 +97,21 @@ export const contextWriteTool = {
|
|
|
87
97
|
title,
|
|
88
98
|
description: input.description,
|
|
89
99
|
content: input.content_base64 ?? input.content,
|
|
90
|
-
|
|
100
|
+
drive: target.drive,
|
|
101
|
+
path: target.path,
|
|
91
102
|
mimeType,
|
|
92
103
|
isTextual,
|
|
93
104
|
});
|
|
94
105
|
|
|
95
|
-
await ingestByPath(ctx.conn,
|
|
96
|
-
const { tree } = await buildContextTree(ctx.conn
|
|
106
|
+
await ingestByPath(ctx.conn, target, ctx.config);
|
|
107
|
+
const { tree } = await buildContextTree(ctx.conn, {
|
|
108
|
+
drive: target.drive,
|
|
109
|
+
});
|
|
97
110
|
return {
|
|
98
111
|
id: item.id,
|
|
99
|
-
|
|
112
|
+
drive: item.drive,
|
|
113
|
+
path: item.path,
|
|
114
|
+
ref: formatDriveRef(item),
|
|
100
115
|
is_error: false,
|
|
101
116
|
tree,
|
|
102
117
|
};
|
|
@@ -104,10 +119,12 @@ export const contextWriteTool = {
|
|
|
104
119
|
if (err instanceof PathConflictError) {
|
|
105
120
|
return {
|
|
106
121
|
id: null,
|
|
107
|
-
|
|
122
|
+
drive: err.drive,
|
|
123
|
+
path: err.path,
|
|
124
|
+
ref: formatDriveRef({ drive: err.drive, path: err.path }),
|
|
108
125
|
is_error: true,
|
|
109
126
|
error_type: "path_conflict",
|
|
110
|
-
message: `A file already exists at ${
|
|
127
|
+
message: `A file already exists at ${formatDriveRef({ drive: err.drive, path: err.path })} (id: ${err.existingId}).`,
|
|
111
128
|
next_action_hint:
|
|
112
129
|
"Call context_read to inspect the existing file, or retry with on_conflict='overwrite' to replace it.",
|
|
113
130
|
};
|
package/src/tools/registry.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Capabilities tools
|
|
2
2
|
import { capabilitiesRefreshTool } from "./capabilities/refresh.ts";
|
|
3
3
|
// Context tools
|
|
4
|
+
import { contextListDrivesTool } from "./context/list-drives.ts";
|
|
4
5
|
import { readLargeResultTool } from "./context/read-large-result.ts";
|
|
5
6
|
import { contextRefreshTool } from "./context/refresh.ts";
|
|
6
7
|
import { contextSearchTool } from "./context/search.ts";
|
|
@@ -8,7 +9,6 @@ import { updateBeliefsTool } from "./context/update-beliefs.ts";
|
|
|
8
9
|
import { updateGoalsTool } from "./context/update-goals.ts";
|
|
9
10
|
// Context — directory operations
|
|
10
11
|
import { contextCreateDirTool } from "./dir/create.ts";
|
|
11
|
-
import { contextListDirTool } from "./dir/list.ts";
|
|
12
12
|
import { contextDirSizeTool } from "./dir/size.ts";
|
|
13
13
|
import { contextTreeTool } from "./dir/tree.ts";
|
|
14
14
|
// Context — file operations
|
|
@@ -58,8 +58,8 @@ export function registerAllTools(): void {
|
|
|
58
58
|
registerTool(viewTaskTool);
|
|
59
59
|
|
|
60
60
|
// Context
|
|
61
|
+
registerTool(contextListDrivesTool);
|
|
61
62
|
registerTool(contextCreateDirTool);
|
|
62
|
-
registerTool(contextListDirTool);
|
|
63
63
|
registerTool(contextTreeTool);
|
|
64
64
|
registerTool(contextDirSizeTool);
|
|
65
65
|
registerTool(contextReadTool);
|