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
package/src/tools/dir/tree.ts
CHANGED
|
@@ -1,237 +1,56 @@
|
|
|
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 base = node.is_directory ? `${node.name}/` : node.name;
|
|
25
|
+
const label = node.is_symlink ? `${base} -> (symlink)` : base;
|
|
26
|
+
lines.push(`${prefix}${connector}${label}`);
|
|
27
|
+
if (node.is_directory && node.children) {
|
|
28
|
+
const childPrefix =
|
|
29
|
+
prefix + (prefix === "" ? "" : isLast ? " " : "│ ");
|
|
30
|
+
const children = node.children;
|
|
31
|
+
children.forEach((c, i) => {
|
|
32
|
+
const last = i === children.length - 1;
|
|
33
|
+
lines.push(...renderTree(c, childPrefix, last));
|
|
156
34
|
});
|
|
157
35
|
}
|
|
36
|
+
return lines;
|
|
37
|
+
}
|
|
158
38
|
|
|
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
|
-
};
|
|
39
|
+
function countItems(node: TreeNode): number {
|
|
40
|
+
if (!node.is_directory) return 1;
|
|
41
|
+
let total = 0;
|
|
42
|
+
for (const c of node.children ?? []) total += countItems(c);
|
|
43
|
+
return total;
|
|
222
44
|
}
|
|
223
45
|
|
|
224
46
|
const inputSchema = z.object({
|
|
225
|
-
|
|
47
|
+
path: z
|
|
226
48
|
.string()
|
|
227
49
|
.optional()
|
|
50
|
+
.default("")
|
|
228
51
|
.describe(
|
|
229
|
-
"
|
|
52
|
+
"Directory path under context/ to render (defaults to the context root).",
|
|
230
53
|
),
|
|
231
|
-
path: z
|
|
232
|
-
.string()
|
|
233
|
-
.optional()
|
|
234
|
-
.describe("Root path for the tree within the drive (defaults to /)"),
|
|
235
54
|
max_depth: z
|
|
236
55
|
.number()
|
|
237
56
|
.int()
|
|
@@ -239,91 +58,67 @@ const inputSchema = z.object({
|
|
|
239
58
|
.optional()
|
|
240
59
|
.default(DEFAULT_MAX_DEPTH)
|
|
241
60
|
.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)".`,
|
|
61
|
+
`Maximum depth of directories to render (defaults to ${DEFAULT_MAX_DEPTH}).`,
|
|
252
62
|
),
|
|
253
63
|
});
|
|
254
64
|
|
|
255
|
-
const TruncatedDirSchema = z.object({
|
|
256
|
-
path: z.string(),
|
|
257
|
-
shown: z.number(),
|
|
258
|
-
total: z.number(),
|
|
259
|
-
});
|
|
260
|
-
|
|
261
65
|
const outputSchema = z.object({
|
|
262
66
|
tree: z.string(),
|
|
263
|
-
is_error: z.boolean(),
|
|
264
67
|
total_items: z.number(),
|
|
265
|
-
|
|
266
|
-
|
|
68
|
+
is_error: z.boolean(),
|
|
69
|
+
error_type: z.string().optional(),
|
|
70
|
+
message: z.string().optional(),
|
|
267
71
|
});
|
|
268
72
|
|
|
269
73
|
export const contextTreeTool = {
|
|
270
74
|
name: "context_tree",
|
|
271
75
|
description:
|
|
272
|
-
"[[ bash equivalent command: tree ]]
|
|
76
|
+
"[[ bash equivalent command: tree ]] Render the file tree under context/ (or a sub-directory). Symlinks are followed for listing and indexing; entries that are symlinks are tagged ' -> (symlink)' in the output.",
|
|
273
77
|
group: "context",
|
|
274
78
|
inputSchema,
|
|
275
79
|
outputSchema,
|
|
276
80
|
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}".`,
|
|
81
|
+
try {
|
|
82
|
+
const node = await buildTree(
|
|
83
|
+
ctx.projectDir,
|
|
84
|
+
input.path ?? "",
|
|
85
|
+
input.max_depth,
|
|
323
86
|
);
|
|
87
|
+
return {
|
|
88
|
+
tree: renderTree(node).join("\n"),
|
|
89
|
+
total_items: countItems(node),
|
|
90
|
+
is_error: false,
|
|
91
|
+
};
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (err instanceof NotFoundError) {
|
|
94
|
+
return {
|
|
95
|
+
tree: "",
|
|
96
|
+
total_items: 0,
|
|
97
|
+
is_error: true,
|
|
98
|
+
error_type: "not_found",
|
|
99
|
+
message: `No path at context/${err.path}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
throw err;
|
|
324
103
|
}
|
|
325
|
-
}
|
|
104
|
+
},
|
|
105
|
+
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
|
326
106
|
|
|
327
|
-
|
|
328
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Convenience for callers that want a string tree from outside the tool layer.
|
|
109
|
+
*/
|
|
110
|
+
export async function buildContextTree(
|
|
111
|
+
projectDir: string,
|
|
112
|
+
opts: { path?: string; maxDepth?: number } = {},
|
|
113
|
+
): Promise<BuildContextTreeResult> {
|
|
114
|
+
const node = await buildTree(
|
|
115
|
+
projectDir,
|
|
116
|
+
opts.path ?? "",
|
|
117
|
+
opts.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
118
|
+
);
|
|
119
|
+
return {
|
|
120
|
+
tree: renderTree(node).join("\n"),
|
|
121
|
+
total_items: countItems(node),
|
|
122
|
+
hint: "",
|
|
123
|
+
};
|
|
329
124
|
}
|
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. Source/destination paths that traverse a user symlink fail with PathEscapeError.",
|
|
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,63 @@ const inputSchema = z.object({
|
|
|
21
20
|
|
|
22
21
|
const outputSchema = z.object({
|
|
23
22
|
deleted: z.number(),
|
|
23
|
+
was_directory: z.boolean(),
|
|
24
|
+
was_symlink: z.boolean(),
|
|
24
25
|
is_error: z.boolean(),
|
|
26
|
+
error_type: z.string().optional(),
|
|
27
|
+
message: z.string().optional(),
|
|
28
|
+
next_action_hint: z.string().optional(),
|
|
25
29
|
});
|
|
26
30
|
|
|
27
31
|
export const contextDeleteTool = {
|
|
28
32
|
name: "context_delete",
|
|
29
33
|
description:
|
|
30
|
-
"[[ bash equivalent command: rm -r ]] Delete a context
|
|
34
|
+
"[[ bash equivalent command: rm -r ]] Delete a file or (with recursive=true) a directory under context/. Symlinks are unlinked without touching their target — `recursive` is not required for a symlinked directory.",
|
|
31
35
|
group: "context",
|
|
32
36
|
inputSchema,
|
|
33
37
|
outputSchema,
|
|
34
38
|
execute: async (input, ctx) => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
try {
|
|
40
|
+
const result = await deleteContextPath(ctx.projectDir, input.path, {
|
|
41
|
+
recursive: input.recursive,
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
deleted: result.removed,
|
|
45
|
+
was_directory: result.was_directory,
|
|
46
|
+
was_symlink: result.was_symlink,
|
|
47
|
+
is_error: false,
|
|
48
|
+
};
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err instanceof NotFoundError) {
|
|
51
|
+
if (input.force) {
|
|
52
|
+
return {
|
|
53
|
+
deleted: 0,
|
|
54
|
+
was_directory: false,
|
|
55
|
+
was_symlink: false,
|
|
56
|
+
is_error: false,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
deleted: 0,
|
|
61
|
+
was_directory: false,
|
|
62
|
+
was_symlink: false,
|
|
63
|
+
is_error: true,
|
|
64
|
+
error_type: "not_found",
|
|
65
|
+
message: `No file at context/${err.path}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (err instanceof IsDirectoryError) {
|
|
69
|
+
return {
|
|
70
|
+
deleted: 0,
|
|
71
|
+
was_directory: true,
|
|
72
|
+
was_symlink: false,
|
|
73
|
+
is_error: true,
|
|
74
|
+
error_type: "is_directory",
|
|
75
|
+
message: `context/${err.path} is a directory`,
|
|
76
|
+
next_action_hint: "Pass recursive=true to delete a directory tree.",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
44
80
|
}
|
|
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
81
|
},
|
|
52
82
|
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|