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/dir/tree.ts
CHANGED
|
@@ -1,237 +1,55 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { formatDriveRef } from "../../context/drives.ts";
|
|
3
|
-
import type { DbConnection } from "../../db/connection.ts";
|
|
4
2
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "../../
|
|
3
|
+
buildTree,
|
|
4
|
+
NotFoundError,
|
|
5
|
+
type TreeNode,
|
|
6
|
+
} from "../../context/store.ts";
|
|
9
7
|
import type { ToolDefinition } from "../tool.ts";
|
|
10
8
|
|
|
11
9
|
const DEFAULT_MAX_DEPTH = 10;
|
|
12
|
-
const DEFAULT_ITEMS_PER_DIR = 15;
|
|
13
|
-
const HARD_FETCH_CAP = 1000;
|
|
14
|
-
|
|
15
|
-
export interface BuildContextTreeOptions {
|
|
16
|
-
drive?: string;
|
|
17
|
-
path?: string;
|
|
18
|
-
maxDepth?: number;
|
|
19
|
-
itemsPerDir?: number;
|
|
20
|
-
}
|
|
21
10
|
|
|
22
11
|
export interface BuildContextTreeResult {
|
|
23
12
|
tree: string;
|
|
24
13
|
total_items: number;
|
|
25
|
-
truncated_dirs: Array<{ path: string; shown: number; total: number }>;
|
|
26
14
|
hint: string;
|
|
27
15
|
}
|
|
28
16
|
|
|
29
|
-
interface DirNode {
|
|
30
|
-
name: string;
|
|
31
|
-
fullPath: string;
|
|
32
|
-
isDir: true;
|
|
33
|
-
children: TreeEntry[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface FileNode {
|
|
37
|
-
name: string;
|
|
38
|
-
fullPath: string;
|
|
39
|
-
isDir: false;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
type TreeEntry = DirNode | FileNode;
|
|
43
|
-
|
|
44
17
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
18
|
+
* Render a TreeNode as an indented string. Files are listed; directories show
|
|
19
|
+
* children. Depth is enforced inside `buildTree`.
|
|
47
20
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
const lines = [
|
|
63
|
-
"Drives:",
|
|
64
|
-
...summaries.map(
|
|
65
|
-
(s) =>
|
|
66
|
-
` ${s.drive}:/ (${s.count} ${s.count === 1 ? "item" : "items"})`,
|
|
67
|
-
),
|
|
68
|
-
];
|
|
69
|
-
const total = summaries.reduce((sum, s) => sum + s.count, 0);
|
|
70
|
-
return {
|
|
71
|
-
tree: lines.join("\n"),
|
|
72
|
-
total_items: total,
|
|
73
|
-
truncated_dirs: [],
|
|
74
|
-
hint: `Call context_tree with drive="<name>" to drill into a drive.`,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const drive = options.drive;
|
|
79
|
-
const path = options.path ?? "/";
|
|
80
|
-
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
81
|
-
const itemsPerDir = options.itemsPerDir ?? DEFAULT_ITEMS_PER_DIR;
|
|
82
|
-
const normalizedPath = path.endsWith("/") ? path : `${path}/`;
|
|
83
|
-
const rootLabel = formatDriveRef({ drive, path });
|
|
84
|
-
|
|
85
|
-
const totalItems = await countContextItemsByPrefix(conn, drive, path, {
|
|
86
|
-
recursive: true,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
if (totalItems === 0) {
|
|
90
|
-
return {
|
|
91
|
-
tree: `${rootLabel}\n (empty)`,
|
|
92
|
-
total_items: 0,
|
|
93
|
-
truncated_dirs: [],
|
|
94
|
-
hint: "Directory is empty.",
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const items = await listContextItemsByPrefix(conn, drive, path, {
|
|
99
|
-
recursive: true,
|
|
100
|
-
limit: HARD_FETCH_CAP,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const root: DirNode = {
|
|
104
|
-
name: rootLabel,
|
|
105
|
-
fullPath: path,
|
|
106
|
-
isDir: true,
|
|
107
|
-
children: [],
|
|
108
|
-
};
|
|
109
|
-
const dirIndex = new Map<string, DirNode>();
|
|
110
|
-
dirIndex.set(stripTrailingSlash(path), root);
|
|
111
|
-
|
|
112
|
-
for (const item of items) {
|
|
113
|
-
const relative = item.path.slice(normalizedPath.length);
|
|
114
|
-
if (relative.length === 0) continue;
|
|
115
|
-
const parts = relative.split("/").filter((p) => p.length > 0);
|
|
116
|
-
const isExplicitDir = item.mime_type === "inode/directory";
|
|
117
|
-
|
|
118
|
-
let parentDir = root;
|
|
119
|
-
let currentRel = "";
|
|
120
|
-
for (let i = 0; i < parts.length; i++) {
|
|
121
|
-
const segment = parts[i];
|
|
122
|
-
if (!segment) continue;
|
|
123
|
-
currentRel = currentRel ? `${currentRel}/${segment}` : segment;
|
|
124
|
-
const fullPath = `${normalizedPath}${currentRel}`;
|
|
125
|
-
const isLeaf = i === parts.length - 1;
|
|
126
|
-
const isDirHere = !isLeaf || isExplicitDir;
|
|
127
|
-
|
|
128
|
-
if (isDirHere) {
|
|
129
|
-
const key = stripTrailingSlash(fullPath);
|
|
130
|
-
let dir = dirIndex.get(key);
|
|
131
|
-
if (!dir) {
|
|
132
|
-
dir = {
|
|
133
|
-
name: segment,
|
|
134
|
-
fullPath,
|
|
135
|
-
isDir: true,
|
|
136
|
-
children: [],
|
|
137
|
-
};
|
|
138
|
-
dirIndex.set(key, dir);
|
|
139
|
-
parentDir.children.push(dir);
|
|
140
|
-
}
|
|
141
|
-
parentDir = dir;
|
|
142
|
-
} else {
|
|
143
|
-
parentDir.children.push({
|
|
144
|
-
name: segment,
|
|
145
|
-
fullPath,
|
|
146
|
-
isDir: false,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
for (const dir of dirIndex.values()) {
|
|
153
|
-
dir.children.sort((a, b) => {
|
|
154
|
-
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
155
|
-
return a.name.localeCompare(b.name);
|
|
21
|
+
function renderTree(node: TreeNode, prefix = "", isLast = true): string[] {
|
|
22
|
+
const lines: string[] = [];
|
|
23
|
+
const connector = prefix === "" ? "" : isLast ? "└── " : "├── ";
|
|
24
|
+
const label = node.is_directory ? `${node.name}/` : node.name;
|
|
25
|
+
lines.push(`${prefix}${connector}${label}`);
|
|
26
|
+
if (node.is_directory && node.children) {
|
|
27
|
+
const childPrefix =
|
|
28
|
+
prefix + (prefix === "" ? "" : isLast ? " " : "│ ");
|
|
29
|
+
const children = node.children;
|
|
30
|
+
children.forEach((c, i) => {
|
|
31
|
+
const last = i === children.length - 1;
|
|
32
|
+
lines.push(...renderTree(c, childPrefix, last));
|
|
156
33
|
});
|
|
157
34
|
}
|
|
35
|
+
return lines;
|
|
36
|
+
}
|
|
158
37
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const depthLimitedDirs: string[] = [];
|
|
165
|
-
|
|
166
|
-
const lines: string[] = [rootLabel];
|
|
167
|
-
|
|
168
|
-
const render = (dir: DirNode, indent: string, currentDepth: number): void => {
|
|
169
|
-
const children = dir.children;
|
|
170
|
-
const total = children.length;
|
|
171
|
-
const shown = Math.min(total, itemsPerDir);
|
|
172
|
-
const visible = children.slice(0, shown);
|
|
173
|
-
const overflow = total - shown;
|
|
174
|
-
|
|
175
|
-
if (overflow > 0) {
|
|
176
|
-
truncatedDirs.push({
|
|
177
|
-
path: stripTrailingSlash(dir.fullPath),
|
|
178
|
-
shown,
|
|
179
|
-
total,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
for (let i = 0; i < visible.length; i++) {
|
|
184
|
-
const child = visible[i];
|
|
185
|
-
if (!child) continue;
|
|
186
|
-
const isLastVisible = i === visible.length - 1 && overflow === 0;
|
|
187
|
-
const connector = isLastVisible ? "└── " : "├── ";
|
|
188
|
-
const childIndent = isLastVisible ? " " : "│ ";
|
|
189
|
-
|
|
190
|
-
if (child.isDir) {
|
|
191
|
-
const atDepthLimit = currentDepth + 1 >= maxDepth;
|
|
192
|
-
if (atDepthLimit && child.children.length > 0) {
|
|
193
|
-
depthLimitedDirs.push(stripTrailingSlash(child.fullPath));
|
|
194
|
-
const subCount = countDescendants(child);
|
|
195
|
-
lines.push(
|
|
196
|
-
`${indent}${connector}${child.name}/ (${subCount} ${
|
|
197
|
-
subCount === 1 ? "item" : "items"
|
|
198
|
-
}, drill in)`,
|
|
199
|
-
);
|
|
200
|
-
} else {
|
|
201
|
-
lines.push(`${indent}${connector}${child.name}/`);
|
|
202
|
-
render(child, indent + childIndent, currentDepth + 1);
|
|
203
|
-
}
|
|
204
|
-
} else {
|
|
205
|
-
lines.push(`${indent}${connector}${child.name}`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (overflow > 0) {
|
|
210
|
-
lines.push(`${indent}└── ... (+${overflow} more)`);
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
render(root, "", 0);
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
tree: lines.join("\n"),
|
|
218
|
-
total_items: totalItems,
|
|
219
|
-
truncated_dirs: truncatedDirs,
|
|
220
|
-
hint: buildHint({ truncatedDirs, depthLimitedDirs, totalItems, drive }),
|
|
221
|
-
};
|
|
38
|
+
function countItems(node: TreeNode): number {
|
|
39
|
+
if (!node.is_directory) return 1;
|
|
40
|
+
let total = 0;
|
|
41
|
+
for (const c of node.children ?? []) total += countItems(c);
|
|
42
|
+
return total;
|
|
222
43
|
}
|
|
223
44
|
|
|
224
45
|
const inputSchema = z.object({
|
|
225
|
-
|
|
46
|
+
path: z
|
|
226
47
|
.string()
|
|
227
48
|
.optional()
|
|
49
|
+
.default("")
|
|
228
50
|
.describe(
|
|
229
|
-
"
|
|
51
|
+
"Directory path under context/ to render (defaults to the context root).",
|
|
230
52
|
),
|
|
231
|
-
path: z
|
|
232
|
-
.string()
|
|
233
|
-
.optional()
|
|
234
|
-
.describe("Root path for the tree within the drive (defaults to /)"),
|
|
235
53
|
max_depth: z
|
|
236
54
|
.number()
|
|
237
55
|
.int()
|
|
@@ -239,91 +57,67 @@ const inputSchema = z.object({
|
|
|
239
57
|
.optional()
|
|
240
58
|
.default(DEFAULT_MAX_DEPTH)
|
|
241
59
|
.describe(
|
|
242
|
-
`Maximum depth of directories to render (defaults to ${DEFAULT_MAX_DEPTH})
|
|
243
|
-
),
|
|
244
|
-
items_per_dir: z
|
|
245
|
-
.number()
|
|
246
|
-
.int()
|
|
247
|
-
.positive()
|
|
248
|
-
.optional()
|
|
249
|
-
.default(DEFAULT_ITEMS_PER_DIR)
|
|
250
|
-
.describe(
|
|
251
|
-
`Maximum entries shown per directory (defaults to ${DEFAULT_ITEMS_PER_DIR}). Overflow shown as "(+N more)".`,
|
|
60
|
+
`Maximum depth of directories to render (defaults to ${DEFAULT_MAX_DEPTH}).`,
|
|
252
61
|
),
|
|
253
62
|
});
|
|
254
63
|
|
|
255
|
-
const TruncatedDirSchema = z.object({
|
|
256
|
-
path: z.string(),
|
|
257
|
-
shown: z.number(),
|
|
258
|
-
total: z.number(),
|
|
259
|
-
});
|
|
260
|
-
|
|
261
64
|
const outputSchema = z.object({
|
|
262
65
|
tree: z.string(),
|
|
263
|
-
is_error: z.boolean(),
|
|
264
66
|
total_items: z.number(),
|
|
265
|
-
|
|
266
|
-
|
|
67
|
+
is_error: z.boolean(),
|
|
68
|
+
error_type: z.string().optional(),
|
|
69
|
+
message: z.string().optional(),
|
|
267
70
|
});
|
|
268
71
|
|
|
269
72
|
export const contextTreeTool = {
|
|
270
73
|
name: "context_tree",
|
|
271
74
|
description:
|
|
272
|
-
"[[ bash equivalent command: tree ]]
|
|
75
|
+
"[[ bash equivalent command: tree ]] Render the file tree under context/ (or a sub-directory).",
|
|
273
76
|
group: "context",
|
|
274
77
|
inputSchema,
|
|
275
78
|
outputSchema,
|
|
276
79
|
execute: async (input, ctx) => {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
});
|
|
283
|
-
return { ...result, is_error: false };
|
|
284
|
-
},
|
|
285
|
-
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
|
286
|
-
|
|
287
|
-
function stripTrailingSlash(p: string): string {
|
|
288
|
-
return p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function countDescendants(dir: DirNode): number {
|
|
292
|
-
let count = 0;
|
|
293
|
-
for (const child of dir.children) {
|
|
294
|
-
count += 1;
|
|
295
|
-
if (child.isDir) count += countDescendants(child);
|
|
296
|
-
}
|
|
297
|
-
return count;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function buildHint(args: {
|
|
301
|
-
truncatedDirs: Array<{ path: string; shown: number; total: number }>;
|
|
302
|
-
depthLimitedDirs: string[];
|
|
303
|
-
totalItems: number;
|
|
304
|
-
drive: string;
|
|
305
|
-
}): string {
|
|
306
|
-
const { truncatedDirs, depthLimitedDirs, drive } = args;
|
|
307
|
-
const parts: string[] = [];
|
|
308
|
-
|
|
309
|
-
if (truncatedDirs.length > 0) {
|
|
310
|
-
const first = truncatedDirs[0];
|
|
311
|
-
if (first) {
|
|
312
|
-
parts.push(
|
|
313
|
-
`${truncatedDirs.length} ${truncatedDirs.length === 1 ? "directory was" : "directories were"} capped by items_per_dir; raise items_per_dir or call context_tree with drive="${drive}", path="${first.path}".`,
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (depthLimitedDirs.length > 0) {
|
|
319
|
-
const first = depthLimitedDirs[0];
|
|
320
|
-
if (first) {
|
|
321
|
-
parts.push(
|
|
322
|
-
`${depthLimitedDirs.length} ${depthLimitedDirs.length === 1 ? "directory was" : "directories were"} not expanded due to max_depth; raise max_depth or call context_tree with drive="${drive}", path="${first}".`,
|
|
80
|
+
try {
|
|
81
|
+
const node = await buildTree(
|
|
82
|
+
ctx.projectDir,
|
|
83
|
+
input.path ?? "",
|
|
84
|
+
input.max_depth,
|
|
323
85
|
);
|
|
86
|
+
return {
|
|
87
|
+
tree: renderTree(node).join("\n"),
|
|
88
|
+
total_items: countItems(node),
|
|
89
|
+
is_error: false,
|
|
90
|
+
};
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (err instanceof NotFoundError) {
|
|
93
|
+
return {
|
|
94
|
+
tree: "",
|
|
95
|
+
total_items: 0,
|
|
96
|
+
is_error: true,
|
|
97
|
+
error_type: "not_found",
|
|
98
|
+
message: `No path at context/${err.path}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
throw err;
|
|
324
102
|
}
|
|
325
|
-
}
|
|
103
|
+
},
|
|
104
|
+
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
|
326
105
|
|
|
327
|
-
|
|
328
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Convenience for callers that want a string tree from outside the tool layer.
|
|
108
|
+
*/
|
|
109
|
+
export async function buildContextTree(
|
|
110
|
+
projectDir: string,
|
|
111
|
+
opts: { path?: string; maxDepth?: number } = {},
|
|
112
|
+
): Promise<BuildContextTreeResult> {
|
|
113
|
+
const node = await buildTree(
|
|
114
|
+
projectDir,
|
|
115
|
+
opts.path ?? "",
|
|
116
|
+
opts.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
117
|
+
);
|
|
118
|
+
return {
|
|
119
|
+
tree: renderTree(node).join("\n"),
|
|
120
|
+
total_items: countItems(node),
|
|
121
|
+
hint: "",
|
|
122
|
+
};
|
|
329
123
|
}
|
package/src/tools/file/copy.ts
CHANGED
|
@@ -1,45 +1,71 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { formatDriveRef } from "../../context/drives.ts";
|
|
3
2
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
copyContextPath,
|
|
4
|
+
deleteContextPath,
|
|
5
|
+
fileExists,
|
|
6
|
+
IsDirectoryError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
PathConflictError,
|
|
9
|
+
} from "../../context/store.ts";
|
|
8
10
|
import type { ToolDefinition } from "../tool.ts";
|
|
9
11
|
|
|
10
12
|
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"),
|
|
13
|
+
src: z.string().describe("Source path under context/"),
|
|
14
|
+
dst: z.string().describe("Destination path under context/"),
|
|
15
15
|
overwrite: z.boolean().optional().describe("Overwrite if destination exists"),
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
const outputSchema = z.object({
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
src: z.string(),
|
|
20
|
+
dst: z.string(),
|
|
21
21
|
is_error: z.boolean(),
|
|
22
|
+
error_type: z.string().optional(),
|
|
23
|
+
message: z.string().optional(),
|
|
22
24
|
});
|
|
23
25
|
|
|
24
26
|
export const contextCopyTool = {
|
|
25
27
|
name: "context_copy",
|
|
26
|
-
description:
|
|
28
|
+
description:
|
|
29
|
+
"[[ bash equivalent command: cp ]] Copy a file under context/ to a new path.",
|
|
27
30
|
group: "context",
|
|
28
31
|
inputSchema,
|
|
29
32
|
outputSchema,
|
|
30
33
|
execute: async (input, ctx) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
try {
|
|
35
|
+
if (input.overwrite && (await fileExists(ctx.projectDir, input.dst))) {
|
|
36
|
+
await deleteContextPath(ctx.projectDir, input.dst);
|
|
37
|
+
}
|
|
38
|
+
await copyContextPath(ctx.projectDir, input.src, input.dst);
|
|
39
|
+
return { src: input.src, dst: input.dst, is_error: false };
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (err instanceof NotFoundError) {
|
|
42
|
+
return {
|
|
43
|
+
src: input.src,
|
|
44
|
+
dst: input.dst,
|
|
45
|
+
is_error: true,
|
|
46
|
+
error_type: "not_found",
|
|
47
|
+
message: `No file at context/${err.path}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (err instanceof PathConflictError) {
|
|
51
|
+
return {
|
|
52
|
+
src: input.src,
|
|
53
|
+
dst: input.dst,
|
|
54
|
+
is_error: true,
|
|
55
|
+
error_type: "path_conflict",
|
|
56
|
+
message: `Destination already exists at context/${err.path}; pass overwrite=true to replace.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (err instanceof IsDirectoryError) {
|
|
60
|
+
return {
|
|
61
|
+
src: input.src,
|
|
62
|
+
dst: input.dst,
|
|
63
|
+
is_error: true,
|
|
64
|
+
error_type: "is_directory",
|
|
65
|
+
message: `Source is a directory: context/${err.path}. Copy is file-only.`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
40
69
|
}
|
|
41
|
-
|
|
42
|
-
const item = await copyContextItem(ctx.conn, src, dst);
|
|
43
|
-
return { id: item.id, ref: formatDriveRef(item), is_error: false };
|
|
44
70
|
},
|
|
45
71
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
|
@@ -1,30 +1,54 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
IsDirectoryError,
|
|
4
|
+
NotFoundError,
|
|
5
|
+
readContextFile,
|
|
6
|
+
} from "../../context/store.ts";
|
|
4
7
|
import type { ToolDefinition } from "../tool.ts";
|
|
5
8
|
|
|
6
9
|
const inputSchema = z.object({
|
|
7
|
-
|
|
8
|
-
path: z.string().describe("Path within the drive"),
|
|
10
|
+
path: z.string().describe("Path under context/"),
|
|
9
11
|
});
|
|
10
12
|
|
|
11
13
|
const outputSchema = z.object({
|
|
12
14
|
lines: z.number(),
|
|
13
15
|
is_error: z.boolean(),
|
|
16
|
+
error_type: z.string().optional(),
|
|
17
|
+
message: z.string().optional(),
|
|
14
18
|
});
|
|
15
19
|
|
|
16
20
|
export const contextCountLinesTool = {
|
|
17
21
|
name: "context_count_lines",
|
|
18
22
|
description:
|
|
19
|
-
"[[ bash equivalent command: wc -l ]] Count the number of lines in a text context
|
|
23
|
+
"[[ bash equivalent command: wc -l ]] Count the number of lines in a text file under context/.",
|
|
20
24
|
group: "context",
|
|
21
25
|
inputSchema,
|
|
22
26
|
outputSchema,
|
|
23
27
|
execute: async (input, ctx) => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
try {
|
|
29
|
+
const content = await readContextFile(ctx.projectDir, input.path);
|
|
30
|
+
return {
|
|
31
|
+
lines: content === "" ? 0 : content.split("\n").length,
|
|
32
|
+
is_error: false,
|
|
33
|
+
};
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (err instanceof NotFoundError) {
|
|
36
|
+
return {
|
|
37
|
+
lines: 0,
|
|
38
|
+
is_error: true,
|
|
39
|
+
error_type: "not_found",
|
|
40
|
+
message: `No file at context/${err.path}`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (err instanceof IsDirectoryError) {
|
|
44
|
+
return {
|
|
45
|
+
lines: 0,
|
|
46
|
+
is_error: true,
|
|
47
|
+
error_type: "is_directory",
|
|
48
|
+
message: `context/${err.path} is a directory`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
29
53
|
},
|
|
30
54
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
package/src/tools/file/delete.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { formatDriveRef } from "../../context/drives.ts";
|
|
3
2
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
deleteContextPath,
|
|
4
|
+
IsDirectoryError,
|
|
5
|
+
NotFoundError,
|
|
6
|
+
} from "../../context/store.ts";
|
|
7
7
|
import type { ToolDefinition } from "../tool.ts";
|
|
8
8
|
|
|
9
9
|
const inputSchema = z.object({
|
|
10
|
-
|
|
11
|
-
path: z.string().describe("Path to delete within the drive"),
|
|
10
|
+
path: z.string().describe("Path under context/ to delete"),
|
|
12
11
|
recursive: z
|
|
13
12
|
.boolean()
|
|
14
13
|
.optional()
|
|
15
|
-
.describe("Delete
|
|
14
|
+
.describe("Delete a directory and its contents recursively"),
|
|
16
15
|
force: z
|
|
17
16
|
.boolean()
|
|
18
17
|
.optional()
|
|
@@ -21,32 +20,54 @@ const inputSchema = z.object({
|
|
|
21
20
|
|
|
22
21
|
const outputSchema = z.object({
|
|
23
22
|
deleted: z.number(),
|
|
23
|
+
was_directory: z.boolean(),
|
|
24
24
|
is_error: z.boolean(),
|
|
25
|
+
error_type: z.string().optional(),
|
|
26
|
+
message: z.string().optional(),
|
|
27
|
+
next_action_hint: z.string().optional(),
|
|
25
28
|
});
|
|
26
29
|
|
|
27
30
|
export const contextDeleteTool = {
|
|
28
31
|
name: "context_delete",
|
|
29
32
|
description:
|
|
30
|
-
"[[ bash equivalent command: rm -r ]] Delete a
|
|
33
|
+
"[[ bash equivalent command: rm -r ]] Delete a file or (with recursive=true) a directory under context/.",
|
|
31
34
|
group: "context",
|
|
32
35
|
inputSchema,
|
|
33
36
|
outputSchema,
|
|
34
37
|
execute: async (input, ctx) => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
try {
|
|
39
|
+
const result = await deleteContextPath(ctx.projectDir, input.path, {
|
|
40
|
+
recursive: input.recursive,
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
deleted: result.removed,
|
|
44
|
+
was_directory: result.was_directory,
|
|
45
|
+
is_error: false,
|
|
46
|
+
};
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (err instanceof NotFoundError) {
|
|
49
|
+
if (input.force) {
|
|
50
|
+
return { deleted: 0, was_directory: false, is_error: false };
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
deleted: 0,
|
|
54
|
+
was_directory: false,
|
|
55
|
+
is_error: true,
|
|
56
|
+
error_type: "not_found",
|
|
57
|
+
message: `No file at context/${err.path}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (err instanceof IsDirectoryError) {
|
|
61
|
+
return {
|
|
62
|
+
deleted: 0,
|
|
63
|
+
was_directory: true,
|
|
64
|
+
is_error: true,
|
|
65
|
+
error_type: "is_directory",
|
|
66
|
+
message: `context/${err.path} is a directory`,
|
|
67
|
+
next_action_hint: "Pass recursive=true to delete a directory tree.",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
44
71
|
}
|
|
45
|
-
|
|
46
|
-
const deleted = await deleteContextItemByPath(ctx.conn, target);
|
|
47
|
-
if (!deleted && !input.force) {
|
|
48
|
-
throw new Error(`Not found: ${formatDriveRef(target)}`);
|
|
49
|
-
}
|
|
50
|
-
return { deleted: deleted ? 1 : 0, is_error: false };
|
|
51
72
|
},
|
|
52
73
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|