@johpaz/hive-tools 1.0.10
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 +42 -0
- package/src/browser/browser.test.ts +111 -0
- package/src/browser/index.ts +272 -0
- package/src/canvas/index.ts +220 -0
- package/src/cron/cron.test.ts +164 -0
- package/src/cron/index.ts +304 -0
- package/src/filesystem/filesystem.test.ts +240 -0
- package/src/filesystem/index.ts +379 -0
- package/src/index.ts +4 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
createCronAddTool,
|
|
4
|
+
createCronListTool,
|
|
5
|
+
createCronRemoveTool,
|
|
6
|
+
createCronEditTool,
|
|
7
|
+
createCronTools,
|
|
8
|
+
getAllCronJobs,
|
|
9
|
+
clearAllCronJobs,
|
|
10
|
+
} from "./index";
|
|
11
|
+
|
|
12
|
+
describe("Cron Tools", () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
clearAllCronJobs();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
clearAllCronJobs();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("createCronAddTool", () => {
|
|
22
|
+
it("creates tool with correct name", () => {
|
|
23
|
+
const tool = createCronAddTool();
|
|
24
|
+
expect(tool.name).toBe("cron_add");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("adds a cron job", async () => {
|
|
28
|
+
const tool = createCronAddTool();
|
|
29
|
+
const result = await tool.execute({
|
|
30
|
+
expression: "0 9 * * *",
|
|
31
|
+
task: "Daily reminder",
|
|
32
|
+
}) as { success: boolean; jobId: string };
|
|
33
|
+
|
|
34
|
+
expect(result.success).toBe(true);
|
|
35
|
+
expect(result.jobId).toMatch(/^cron-\d+$/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("rejects invalid cron expression", async () => {
|
|
39
|
+
const tool = createCronAddTool();
|
|
40
|
+
await expect(
|
|
41
|
+
tool.execute({
|
|
42
|
+
expression: "invalid",
|
|
43
|
+
task: "Test task",
|
|
44
|
+
})
|
|
45
|
+
).rejects.toThrow("Cron expression must have 5 fields");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("rejects when max jobs reached", async () => {
|
|
49
|
+
const tool = createCronAddTool({ maxJobs: 1 });
|
|
50
|
+
await tool.execute({
|
|
51
|
+
expression: "* * * * *",
|
|
52
|
+
task: "Task 1",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await expect(
|
|
56
|
+
tool.execute({
|
|
57
|
+
expression: "* * * * *",
|
|
58
|
+
task: "Task 2",
|
|
59
|
+
})
|
|
60
|
+
).rejects.toThrow("Maximum number of cron jobs reached");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("createCronListTool", () => {
|
|
65
|
+
it("creates tool with correct name", () => {
|
|
66
|
+
const tool = createCronListTool();
|
|
67
|
+
expect(tool.name).toBe("cron_list");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("lists all cron jobs", async () => {
|
|
71
|
+
const addTool = createCronAddTool();
|
|
72
|
+
await addTool.execute({
|
|
73
|
+
expression: "* * * * *",
|
|
74
|
+
task: "Task 1",
|
|
75
|
+
});
|
|
76
|
+
await addTool.execute({
|
|
77
|
+
expression: "0 0 * * *",
|
|
78
|
+
task: "Task 2",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const listTool = createCronListTool();
|
|
82
|
+
const result = await listTool.execute({}) as { success: boolean; jobs: unknown[]; count: number };
|
|
83
|
+
|
|
84
|
+
expect(result.success).toBe(true);
|
|
85
|
+
expect(result.count).toBe(2);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("createCronRemoveTool", () => {
|
|
90
|
+
it("creates tool with correct name", () => {
|
|
91
|
+
const tool = createCronRemoveTool();
|
|
92
|
+
expect(tool.name).toBe("cron_remove");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("removes a cron job", async () => {
|
|
96
|
+
const addTool = createCronAddTool();
|
|
97
|
+
const addResult = await addTool.execute({
|
|
98
|
+
expression: "* * * * *",
|
|
99
|
+
task: "To remove",
|
|
100
|
+
}) as { jobId: string };
|
|
101
|
+
|
|
102
|
+
const removeTool = createCronRemoveTool();
|
|
103
|
+
const result = await removeTool.execute({
|
|
104
|
+
jobId: addResult.jobId,
|
|
105
|
+
}) as { success: boolean; removedJob: string };
|
|
106
|
+
|
|
107
|
+
expect(result.success).toBe(true);
|
|
108
|
+
expect(result.removedJob).toBe(addResult.jobId);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("throws for non-existent job", async () => {
|
|
112
|
+
const tool = createCronRemoveTool();
|
|
113
|
+
await expect(
|
|
114
|
+
tool.execute({ jobId: "non-existent" })
|
|
115
|
+
).rejects.toThrow("Job not found");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("createCronEditTool", () => {
|
|
120
|
+
it("creates tool with correct name", () => {
|
|
121
|
+
const tool = createCronEditTool();
|
|
122
|
+
expect(tool.name).toBe("cron_edit");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("edits a cron job", async () => {
|
|
126
|
+
const addTool = createCronAddTool();
|
|
127
|
+
const addResult = await addTool.execute({
|
|
128
|
+
expression: "* * * * *",
|
|
129
|
+
task: "Original",
|
|
130
|
+
}) as { jobId: string };
|
|
131
|
+
|
|
132
|
+
const editTool = createCronEditTool();
|
|
133
|
+
const result = await editTool.execute({
|
|
134
|
+
jobId: addResult.jobId,
|
|
135
|
+
task: "Updated",
|
|
136
|
+
enabled: false,
|
|
137
|
+
}) as { success: boolean; job: { task: string; enabled: boolean } };
|
|
138
|
+
|
|
139
|
+
expect(result.success).toBe(true);
|
|
140
|
+
expect(result.job.task).toBe("Updated");
|
|
141
|
+
expect(result.job.enabled).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("throws for non-existent job", async () => {
|
|
145
|
+
const tool = createCronEditTool();
|
|
146
|
+
await expect(
|
|
147
|
+
tool.execute({ jobId: "non-existent" })
|
|
148
|
+
).rejects.toThrow("Job not found");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("createCronTools", () => {
|
|
153
|
+
it("returns all cron tools", () => {
|
|
154
|
+
const tools = createCronTools();
|
|
155
|
+
expect(tools.length).toBe(4);
|
|
156
|
+
expect(tools.map((t) => t.name)).toEqual([
|
|
157
|
+
"cron_add",
|
|
158
|
+
"cron_list",
|
|
159
|
+
"cron_remove",
|
|
160
|
+
"cron_edit",
|
|
161
|
+
]);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import type { Tool } from "@johpaz/hive-core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export interface CronConfig {
|
|
5
|
+
maxJobs?: number;
|
|
6
|
+
defaultTimeout?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface CronJob {
|
|
10
|
+
id: string;
|
|
11
|
+
expression: string;
|
|
12
|
+
task: string;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
createdAt: Date;
|
|
15
|
+
lastRun?: Date;
|
|
16
|
+
nextRun?: Date;
|
|
17
|
+
data?: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const cronJobs: Map<string, CronJob> = new Map();
|
|
21
|
+
let jobCounter = 0;
|
|
22
|
+
let schedulerInterval: ReturnType<typeof setInterval> | null = null;
|
|
23
|
+
|
|
24
|
+
function parseCronExpression(expr: string): { valid: boolean; error?: string } {
|
|
25
|
+
const parts = expr.trim().split(/\s+/);
|
|
26
|
+
if (parts.length !== 5) {
|
|
27
|
+
return { valid: false, error: "Cron expression must have 5 fields (minute hour day month weekday)" };
|
|
28
|
+
}
|
|
29
|
+
return { valid: true };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function matchCron(expr: string, date: Date): boolean {
|
|
33
|
+
const parts = expr.split(/\s+/);
|
|
34
|
+
if (parts.length !== 5) return false;
|
|
35
|
+
|
|
36
|
+
const [min, hour, dom, month, dow] = parts;
|
|
37
|
+
|
|
38
|
+
const match = (val: number, p: string): boolean => {
|
|
39
|
+
if (p === "*") return true;
|
|
40
|
+
if (p.startsWith("*/")) {
|
|
41
|
+
const step = parseInt(p.slice(2), 10);
|
|
42
|
+
return step > 0 && val % step === 0;
|
|
43
|
+
}
|
|
44
|
+
if (p.includes(",")) {
|
|
45
|
+
return p.split(",").some(v => match(val, v));
|
|
46
|
+
}
|
|
47
|
+
if (p.includes("-")) {
|
|
48
|
+
const [start, end] = p.split("-").map(Number);
|
|
49
|
+
return val >= start! && val <= end!;
|
|
50
|
+
}
|
|
51
|
+
return parseInt(p, 10) === val;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
match(date.getMinutes(), min!) &&
|
|
56
|
+
match(date.getHours(), hour!) &&
|
|
57
|
+
match(date.getDate(), dom!) &&
|
|
58
|
+
match(date.getMonth() + 1, month!) &&
|
|
59
|
+
match(date.getDay(), dow!)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getNextRun(expression: string): Date | undefined {
|
|
64
|
+
const now = new Date();
|
|
65
|
+
for (let i = 1; i <= 525600; i++) {
|
|
66
|
+
const next = new Date(now.getTime() + i * 60000);
|
|
67
|
+
if (matchCron(expression, next)) {
|
|
68
|
+
return next;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function startScheduler(onTrigger: (job: CronJob) => void): void {
|
|
75
|
+
if (schedulerInterval) return;
|
|
76
|
+
|
|
77
|
+
schedulerInterval = setInterval(() => {
|
|
78
|
+
const now = new Date();
|
|
79
|
+
const currentMinute = Math.floor(now.getTime() / 60000);
|
|
80
|
+
|
|
81
|
+
for (const job of cronJobs.values()) {
|
|
82
|
+
if (!job.enabled) continue;
|
|
83
|
+
|
|
84
|
+
const lastRunMinute = job.lastRun ? Math.floor(job.lastRun.getTime() / 60000) : 0;
|
|
85
|
+
|
|
86
|
+
if (lastRunMinute === currentMinute) continue;
|
|
87
|
+
|
|
88
|
+
if (matchCron(job.expression, now)) {
|
|
89
|
+
job.lastRun = now;
|
|
90
|
+
job.nextRun = getNextRun(job.expression);
|
|
91
|
+
onTrigger(job);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}, 60000);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createCronAddTool(config: CronConfig = {}): Tool {
|
|
98
|
+
return {
|
|
99
|
+
name: "cron_add",
|
|
100
|
+
description: "Add a scheduled task with cron expression",
|
|
101
|
+
parameters: {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: {
|
|
104
|
+
expression: {
|
|
105
|
+
type: "string",
|
|
106
|
+
description: "Cron expression (e.g., '0 9 * * *' for daily at 9am)",
|
|
107
|
+
},
|
|
108
|
+
task: {
|
|
109
|
+
type: "string",
|
|
110
|
+
description: "Task description or command to execute",
|
|
111
|
+
},
|
|
112
|
+
data: {
|
|
113
|
+
type: "object",
|
|
114
|
+
description: "Additional data for the task",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
required: ["expression", "task"],
|
|
118
|
+
},
|
|
119
|
+
execute: async (params: Record<string, unknown>) => {
|
|
120
|
+
const expression = params.expression as string;
|
|
121
|
+
const task = params.task as string;
|
|
122
|
+
const data = params.data;
|
|
123
|
+
|
|
124
|
+
if (cronJobs.size >= (config.maxJobs || 100)) {
|
|
125
|
+
throw new Error("Maximum number of cron jobs reached");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const validation = parseCronExpression(expression);
|
|
129
|
+
if (!validation.valid) {
|
|
130
|
+
throw new Error(validation.error);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const id = `cron-${++jobCounter}`;
|
|
134
|
+
const job: CronJob = {
|
|
135
|
+
id,
|
|
136
|
+
expression,
|
|
137
|
+
task,
|
|
138
|
+
enabled: true,
|
|
139
|
+
createdAt: new Date(),
|
|
140
|
+
nextRun: getNextRun(expression),
|
|
141
|
+
data,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
cronJobs.set(id, job);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
jobId: id,
|
|
149
|
+
job: {
|
|
150
|
+
id: job.id,
|
|
151
|
+
expression: job.expression,
|
|
152
|
+
task: job.task.slice(0, 100),
|
|
153
|
+
enabled: job.enabled,
|
|
154
|
+
nextRun: job.nextRun?.toISOString(),
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function createCronListTool(): Tool {
|
|
162
|
+
return {
|
|
163
|
+
name: "cron_list",
|
|
164
|
+
description: "List all scheduled cron jobs",
|
|
165
|
+
parameters: {
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {},
|
|
168
|
+
},
|
|
169
|
+
execute: async () => {
|
|
170
|
+
const jobs = Array.from(cronJobs.values()).map(job => ({
|
|
171
|
+
id: job.id,
|
|
172
|
+
expression: job.expression,
|
|
173
|
+
task: job.task.slice(0, 100),
|
|
174
|
+
enabled: job.enabled,
|
|
175
|
+
lastRun: job.lastRun?.toISOString(),
|
|
176
|
+
nextRun: job.nextRun?.toISOString(),
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
success: true,
|
|
181
|
+
jobs,
|
|
182
|
+
count: jobs.length,
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function createCronRemoveTool(): Tool {
|
|
189
|
+
return {
|
|
190
|
+
name: "cron_remove",
|
|
191
|
+
description: "Remove a scheduled cron job",
|
|
192
|
+
parameters: {
|
|
193
|
+
type: "object",
|
|
194
|
+
properties: {
|
|
195
|
+
jobId: {
|
|
196
|
+
type: "string",
|
|
197
|
+
description: "The ID of the job to remove",
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
required: ["jobId"],
|
|
201
|
+
},
|
|
202
|
+
execute: async (params: Record<string, unknown>) => {
|
|
203
|
+
const jobId = params.jobId as string;
|
|
204
|
+
|
|
205
|
+
if (!cronJobs.has(jobId)) {
|
|
206
|
+
throw new Error(`Job not found: ${jobId}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
cronJobs.delete(jobId);
|
|
210
|
+
|
|
211
|
+
return { success: true, removedJob: jobId };
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function createCronEditTool(): Tool {
|
|
217
|
+
return {
|
|
218
|
+
name: "cron_edit",
|
|
219
|
+
description: "Edit a scheduled cron job",
|
|
220
|
+
parameters: {
|
|
221
|
+
type: "object",
|
|
222
|
+
properties: {
|
|
223
|
+
jobId: {
|
|
224
|
+
type: "string",
|
|
225
|
+
description: "The ID of the job to edit",
|
|
226
|
+
},
|
|
227
|
+
expression: {
|
|
228
|
+
type: "string",
|
|
229
|
+
description: "New cron expression",
|
|
230
|
+
},
|
|
231
|
+
task: {
|
|
232
|
+
type: "string",
|
|
233
|
+
description: "New task description",
|
|
234
|
+
},
|
|
235
|
+
enabled: {
|
|
236
|
+
type: "boolean",
|
|
237
|
+
description: "Enable or disable the job",
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
required: ["jobId"],
|
|
241
|
+
},
|
|
242
|
+
execute: async (params: Record<string, unknown>) => {
|
|
243
|
+
const jobId = params.jobId as string;
|
|
244
|
+
const job = cronJobs.get(jobId);
|
|
245
|
+
|
|
246
|
+
if (!job) {
|
|
247
|
+
throw new Error(`Job not found: ${jobId}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (params.expression) {
|
|
251
|
+
const validation = parseCronExpression(params.expression as string);
|
|
252
|
+
if (!validation.valid) {
|
|
253
|
+
throw new Error(validation.error);
|
|
254
|
+
}
|
|
255
|
+
job.expression = params.expression as string;
|
|
256
|
+
job.nextRun = getNextRun(job.expression);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (params.task) {
|
|
260
|
+
job.task = params.task as string;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (params.enabled !== undefined) {
|
|
264
|
+
job.enabled = params.enabled as boolean;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
success: true,
|
|
269
|
+
job: {
|
|
270
|
+
id: job.id,
|
|
271
|
+
expression: job.expression,
|
|
272
|
+
task: job.task.slice(0, 100),
|
|
273
|
+
enabled: job.enabled,
|
|
274
|
+
nextRun: job.nextRun?.toISOString(),
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function createCronTools(config: CronConfig = {}, onTrigger?: (job: CronJob) => void): Tool[] {
|
|
282
|
+
if (onTrigger) {
|
|
283
|
+
startScheduler(onTrigger);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return [
|
|
287
|
+
createCronAddTool(config),
|
|
288
|
+
createCronListTool(),
|
|
289
|
+
createCronRemoveTool(),
|
|
290
|
+
createCronEditTool(),
|
|
291
|
+
];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function getAllCronJobs(): CronJob[] {
|
|
295
|
+
return Array.from(cronJobs.values());
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function clearAllCronJobs(): void {
|
|
299
|
+
cronJobs.clear();
|
|
300
|
+
if (schedulerInterval) {
|
|
301
|
+
clearInterval(schedulerInterval);
|
|
302
|
+
schedulerInterval = null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import {
|
|
6
|
+
createFSReadTool,
|
|
7
|
+
createFSWriteTool,
|
|
8
|
+
createFSListTool,
|
|
9
|
+
createFSMkdirTool,
|
|
10
|
+
createFSDeleteTool,
|
|
11
|
+
createFSCopyTool,
|
|
12
|
+
createFSMoveTool,
|
|
13
|
+
createFSTools,
|
|
14
|
+
} from "./index";
|
|
15
|
+
|
|
16
|
+
describe("FileSystem Tools", () => {
|
|
17
|
+
let tempDir: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "fs-tools-test-"));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("createFSReadTool", () => {
|
|
28
|
+
it("creates tool with correct name", () => {
|
|
29
|
+
const tool = createFSReadTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
30
|
+
expect(tool.name).toBe("fs_read");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("reads a file", async () => {
|
|
34
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
35
|
+
await fs.writeFile(testFile, "Hello, World!");
|
|
36
|
+
|
|
37
|
+
const tool = createFSReadTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
38
|
+
const result = await tool.execute({ path: testFile }) as { success: boolean; content: string };
|
|
39
|
+
|
|
40
|
+
expect(result.success).toBe(true);
|
|
41
|
+
expect(result.content).toBe("Hello, World!");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("reads file with line range", async () => {
|
|
45
|
+
const testFile = path.join(tempDir, "lines.txt");
|
|
46
|
+
await fs.writeFile(testFile, "line1\nline2\nline3\nline4\nline5");
|
|
47
|
+
|
|
48
|
+
const tool = createFSReadTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
49
|
+
const result = await tool.execute({
|
|
50
|
+
path: testFile,
|
|
51
|
+
startLine: 2,
|
|
52
|
+
endLine: 4,
|
|
53
|
+
}) as { success: boolean; content: string };
|
|
54
|
+
|
|
55
|
+
expect(result.success).toBe(true);
|
|
56
|
+
expect(result.content).toBe("line2\nline3\nline4");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("rejects path outside allowed paths when sandboxed", async () => {
|
|
60
|
+
const tool = createFSReadTool({ allowedPaths: [tempDir], sandboxed: true });
|
|
61
|
+
await expect(
|
|
62
|
+
tool.execute({ path: "/etc/passwd" })
|
|
63
|
+
).rejects.toThrow("Path not allowed");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("createFSWriteTool", () => {
|
|
68
|
+
it("creates tool with correct name", () => {
|
|
69
|
+
const tool = createFSWriteTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
70
|
+
expect(tool.name).toBe("fs_write");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("writes a file", async () => {
|
|
74
|
+
const testFile = path.join(tempDir, "write-test.txt");
|
|
75
|
+
|
|
76
|
+
const tool = createFSWriteTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
77
|
+
const result = await tool.execute({
|
|
78
|
+
path: testFile,
|
|
79
|
+
content: "Test content",
|
|
80
|
+
}) as { success: boolean; bytesWritten: number };
|
|
81
|
+
|
|
82
|
+
expect(result.success).toBe(true);
|
|
83
|
+
expect(result.bytesWritten).toBe(12);
|
|
84
|
+
|
|
85
|
+
const content = await fs.readFile(testFile, "utf-8");
|
|
86
|
+
expect(content).toBe("Test content");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("appends to a file", async () => {
|
|
90
|
+
const testFile = path.join(tempDir, "append-test.txt");
|
|
91
|
+
await fs.writeFile(testFile, "Initial ");
|
|
92
|
+
|
|
93
|
+
const tool = createFSWriteTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
94
|
+
await tool.execute({
|
|
95
|
+
path: testFile,
|
|
96
|
+
content: "Appended",
|
|
97
|
+
mode: "append",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const content = await fs.readFile(testFile, "utf-8");
|
|
101
|
+
expect(content).toBe("Initial Appended");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("createFSListTool", () => {
|
|
106
|
+
it("creates tool with correct name", () => {
|
|
107
|
+
const tool = createFSListTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
108
|
+
expect(tool.name).toBe("fs_list");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("lists directory contents", async () => {
|
|
112
|
+
await fs.writeFile(path.join(tempDir, "file1.txt"), "content1");
|
|
113
|
+
await fs.writeFile(path.join(tempDir, "file2.txt"), "content2");
|
|
114
|
+
await fs.mkdir(path.join(tempDir, "subdir"));
|
|
115
|
+
|
|
116
|
+
const tool = createFSListTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
117
|
+
const result = await tool.execute({ path: tempDir }) as { success: boolean; entries: Array<{ name: string; type: string }>; count: number };
|
|
118
|
+
|
|
119
|
+
expect(result.success).toBe(true);
|
|
120
|
+
expect(result.count).toBe(3);
|
|
121
|
+
expect(result.entries.find((e) => e.name === "file1.txt")?.type).toBe("file");
|
|
122
|
+
expect(result.entries.find((e) => e.name === "subdir")?.type).toBe("directory");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("createFSMkdirTool", () => {
|
|
127
|
+
it("creates tool with correct name", () => {
|
|
128
|
+
const tool = createFSMkdirTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
129
|
+
expect(tool.name).toBe("fs_mkdir");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("creates a directory", async () => {
|
|
133
|
+
const newDir = path.join(tempDir, "new-dir");
|
|
134
|
+
|
|
135
|
+
const tool = createFSMkdirTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
136
|
+
const result = await tool.execute({ path: newDir }) as { success: boolean };
|
|
137
|
+
|
|
138
|
+
expect(result.success).toBe(true);
|
|
139
|
+
const stat = await fs.stat(newDir);
|
|
140
|
+
expect(stat.isDirectory()).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("creates nested directories", async () => {
|
|
144
|
+
const nestedDir = path.join(tempDir, "a", "b", "c");
|
|
145
|
+
|
|
146
|
+
const tool = createFSMkdirTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
147
|
+
await tool.execute({ path: nestedDir });
|
|
148
|
+
|
|
149
|
+
const stat = await fs.stat(nestedDir);
|
|
150
|
+
expect(stat.isDirectory()).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("createFSDeleteTool", () => {
|
|
155
|
+
it("creates tool with correct name", () => {
|
|
156
|
+
const tool = createFSDeleteTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
157
|
+
expect(tool.name).toBe("fs_delete");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("deletes a file", async () => {
|
|
161
|
+
const testFile = path.join(tempDir, "to-delete.txt");
|
|
162
|
+
await fs.writeFile(testFile, "content");
|
|
163
|
+
|
|
164
|
+
const tool = createFSDeleteTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
165
|
+
const result = await tool.execute({ path: testFile }) as { success: boolean };
|
|
166
|
+
|
|
167
|
+
expect(result.success).toBe(true);
|
|
168
|
+
await expect(fs.stat(testFile)).rejects.toThrow();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("deletes a directory recursively", async () => {
|
|
172
|
+
const testDir = path.join(tempDir, "to-delete-dir");
|
|
173
|
+
await fs.mkdir(testDir);
|
|
174
|
+
await fs.writeFile(path.join(testDir, "file.txt"), "content");
|
|
175
|
+
|
|
176
|
+
const tool = createFSDeleteTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
177
|
+
const result = await tool.execute({ path: testDir, recursive: true }) as { success: boolean };
|
|
178
|
+
|
|
179
|
+
expect(result.success).toBe(true);
|
|
180
|
+
await expect(fs.stat(testDir)).rejects.toThrow();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("createFSCopyTool", () => {
|
|
185
|
+
it("creates tool with correct name", () => {
|
|
186
|
+
const tool = createFSCopyTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
187
|
+
expect(tool.name).toBe("fs_copy");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("copies a file", async () => {
|
|
191
|
+
const source = path.join(tempDir, "source.txt");
|
|
192
|
+
const dest = path.join(tempDir, "dest.txt");
|
|
193
|
+
await fs.writeFile(source, "content");
|
|
194
|
+
|
|
195
|
+
const tool = createFSCopyTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
196
|
+
const result = await tool.execute({ source, destination: dest }) as { success: boolean };
|
|
197
|
+
|
|
198
|
+
expect(result.success).toBe(true);
|
|
199
|
+
const content = await fs.readFile(dest, "utf-8");
|
|
200
|
+
expect(content).toBe("content");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("createFSMoveTool", () => {
|
|
205
|
+
it("creates tool with correct name", () => {
|
|
206
|
+
const tool = createFSMoveTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
207
|
+
expect(tool.name).toBe("fs_move");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("moves a file", async () => {
|
|
211
|
+
const source = path.join(tempDir, "source.txt");
|
|
212
|
+
const dest = path.join(tempDir, "dest.txt");
|
|
213
|
+
await fs.writeFile(source, "content");
|
|
214
|
+
|
|
215
|
+
const tool = createFSMoveTool({ allowedPaths: [tempDir], sandboxed: false });
|
|
216
|
+
const result = await tool.execute({ source, destination: dest }) as { success: boolean };
|
|
217
|
+
|
|
218
|
+
expect(result.success).toBe(true);
|
|
219
|
+
await expect(fs.stat(source)).rejects.toThrow();
|
|
220
|
+
const content = await fs.readFile(dest, "utf-8");
|
|
221
|
+
expect(content).toBe("content");
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("createFSTools", () => {
|
|
226
|
+
it("returns all filesystem tools", () => {
|
|
227
|
+
const tools = createFSTools({ allowedPaths: [tempDir], sandboxed: false });
|
|
228
|
+
expect(tools.length).toBe(7);
|
|
229
|
+
expect(tools.map((t) => t.name)).toEqual([
|
|
230
|
+
"fs_read",
|
|
231
|
+
"fs_write",
|
|
232
|
+
"fs_list",
|
|
233
|
+
"fs_mkdir",
|
|
234
|
+
"fs_delete",
|
|
235
|
+
"fs_copy",
|
|
236
|
+
"fs_move",
|
|
237
|
+
]);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|