pretticlaw 0.1.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/CONTRIBUTING.md +123 -0
- package/README.md +150 -0
- package/assets/logo.png +0 -0
- package/dist/agent/context.d.ts +22 -0
- package/dist/agent/context.js +85 -0
- package/dist/agent/loop.d.ts +63 -0
- package/dist/agent/loop.js +244 -0
- package/dist/agent/memory.d.ts +16 -0
- package/dist/agent/memory.js +98 -0
- package/dist/agent/skills.d.ts +18 -0
- package/dist/agent/skills.js +121 -0
- package/dist/agent/subagent.d.ts +30 -0
- package/dist/agent/subagent.js +92 -0
- package/dist/agent/tools/base.d.ts +10 -0
- package/dist/agent/tools/base.js +58 -0
- package/dist/agent/tools/cron.d.ts +43 -0
- package/dist/agent/tools/cron.js +83 -0
- package/dist/agent/tools/filesystem.d.ts +79 -0
- package/dist/agent/tools/filesystem.js +125 -0
- package/dist/agent/tools/message.d.ts +41 -0
- package/dist/agent/tools/message.js +55 -0
- package/dist/agent/tools/registry.d.ts +9 -0
- package/dist/agent/tools/registry.js +33 -0
- package/dist/agent/tools/shell.d.ts +26 -0
- package/dist/agent/tools/shell.js +78 -0
- package/dist/agent/tools/spawn.d.ts +27 -0
- package/dist/agent/tools/spawn.js +35 -0
- package/dist/agent/tools/web.d.ts +50 -0
- package/dist/agent/tools/web.js +119 -0
- package/dist/bus/async-queue.d.ts +7 -0
- package/dist/bus/async-queue.js +20 -0
- package/dist/bus/events.d.ts +19 -0
- package/dist/bus/events.js +3 -0
- package/dist/bus/queue.d.ts +12 -0
- package/dist/bus/queue.js +23 -0
- package/dist/channels/base.d.ts +22 -0
- package/dist/channels/base.js +35 -0
- package/dist/channels/discord.d.ts +24 -0
- package/dist/channels/discord.js +133 -0
- package/dist/channels/manager.d.ts +17 -0
- package/dist/channels/manager.js +67 -0
- package/dist/channels/stub.d.ts +10 -0
- package/dist/channels/stub.js +18 -0
- package/dist/channels/telegram.d.ts +20 -0
- package/dist/channels/telegram.js +93 -0
- package/dist/cli/commands.d.ts +2 -0
- package/dist/cli/commands.js +552 -0
- package/dist/config/loader.d.ts +5 -0
- package/dist/config/loader.js +55 -0
- package/dist/config/schema.d.ts +246 -0
- package/dist/config/schema.js +94 -0
- package/dist/cron/service.d.ts +33 -0
- package/dist/cron/service.js +195 -0
- package/dist/cron/types.d.ts +47 -0
- package/dist/cron/types.js +1 -0
- package/dist/dashboard/index.html +1567 -0
- package/dist/heartbeat/service.d.ts +21 -0
- package/dist/heartbeat/service.js +101 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/providers/base.d.ts +23 -0
- package/dist/providers/base.js +21 -0
- package/dist/providers/custom-provider.d.ts +16 -0
- package/dist/providers/custom-provider.js +49 -0
- package/dist/providers/litellm-provider.d.ts +19 -0
- package/dist/providers/litellm-provider.js +128 -0
- package/dist/providers/registry.d.ts +5 -0
- package/dist/providers/registry.js +45 -0
- package/dist/session/manager.d.ts +31 -0
- package/dist/session/manager.js +116 -0
- package/dist/skills/README.md +25 -0
- package/dist/skills/clawhub/SKILL.md +53 -0
- package/dist/skills/cron/SKILL.md +57 -0
- package/dist/skills/github/SKILL.md +48 -0
- package/dist/skills/memory/SKILL.md +31 -0
- package/dist/skills/skill-creator/SKILL.md +371 -0
- package/dist/skills/summarize/SKILL.md +67 -0
- package/dist/skills/tmux/SKILL.md +121 -0
- package/dist/skills/tmux/scripts/find-sessions.sh +112 -0
- package/dist/skills/tmux/scripts/wait-for-text.sh +83 -0
- package/dist/skills/weather/SKILL.md +49 -0
- package/dist/templates/AGENTS.md +23 -0
- package/dist/templates/HEARTBEAT.md +16 -0
- package/dist/templates/SOUL.md +21 -0
- package/dist/templates/TOOLS.md +15 -0
- package/dist/templates/USER.md +49 -0
- package/dist/templates/memory/MEMORY.md +23 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +3 -0
- package/dist/utils/helpers.d.ts +5 -0
- package/dist/utils/helpers.js +53 -0
- package/dist/web/server.d.ts +15 -0
- package/dist/web/server.js +169 -0
- package/package.json +37 -0
- package/scripts/copy-assets.mjs +21 -0
- package/src/agent/context.ts +90 -0
- package/src/agent/loop.ts +291 -0
- package/src/agent/memory.ts +104 -0
- package/src/agent/skills.ts +121 -0
- package/src/agent/subagent.ts +96 -0
- package/src/agent/tools/base.ts +59 -0
- package/src/agent/tools/cron.ts +79 -0
- package/src/agent/tools/filesystem.ts +93 -0
- package/src/agent/tools/message.ts +57 -0
- package/src/agent/tools/registry.ts +36 -0
- package/src/agent/tools/shell.ts +69 -0
- package/src/agent/tools/spawn.ts +37 -0
- package/src/agent/tools/web.ts +108 -0
- package/src/bus/async-queue.ts +20 -0
- package/src/bus/events.ts +23 -0
- package/src/bus/queue.ts +31 -0
- package/src/channels/base.ts +36 -0
- package/src/channels/discord.ts +156 -0
- package/src/channels/manager.ts +70 -0
- package/src/channels/stub.ts +20 -0
- package/src/channels/telegram.ts +120 -0
- package/src/cli/commands.ts +581 -0
- package/src/config/loader.ts +58 -0
- package/src/config/schema.ts +144 -0
- package/src/cron/service.ts +190 -0
- package/src/cron/types.ts +36 -0
- package/src/dashboard/index.html +1567 -0
- package/src/heartbeat/service.ts +95 -0
- package/src/index.ts +6 -0
- package/src/providers/base.ts +43 -0
- package/src/providers/custom-provider.ts +46 -0
- package/src/providers/litellm-provider.ts +131 -0
- package/src/providers/registry.ts +48 -0
- package/src/session/manager.ts +129 -0
- package/src/skills/README.md +25 -0
- package/src/skills/clawhub/SKILL.md +53 -0
- package/src/skills/cron/SKILL.md +57 -0
- package/src/skills/github/SKILL.md +48 -0
- package/src/skills/memory/SKILL.md +31 -0
- package/src/skills/skill-creator/SKILL.md +371 -0
- package/src/skills/summarize/SKILL.md +67 -0
- package/src/skills/tmux/SKILL.md +121 -0
- package/src/skills/tmux/scripts/find-sessions.sh +112 -0
- package/src/skills/tmux/scripts/wait-for-text.sh +83 -0
- package/src/skills/weather/SKILL.md +49 -0
- package/src/templates/AGENTS.md +23 -0
- package/src/templates/HEARTBEAT.md +16 -0
- package/src/templates/SOUL.md +21 -0
- package/src/templates/TOOLS.md +15 -0
- package/src/templates/USER.md +49 -0
- package/src/templates/memory/MEMORY.md +23 -0
- package/src/types/prompts.d.ts +14 -0
- package/src/types/ws.d.ts +15 -0
- package/src/types.ts +5 -0
- package/src/utils/helpers.ts +55 -0
- package/src/web/server.ts +198 -0
- package/test/context.test.ts +27 -0
- package/test/cron-service.test.ts +31 -0
- package/test/message-tool.test.ts +10 -0
- package/test/providers.test.ts +43 -0
- package/test/tool-validation.test.ts +61 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export class Tool {
|
|
2
|
+
static typeMap = {
|
|
3
|
+
string: (v) => typeof v === "string",
|
|
4
|
+
integer: (v) => Number.isInteger(v),
|
|
5
|
+
number: (v) => typeof v === "number" && Number.isFinite(v),
|
|
6
|
+
boolean: (v) => typeof v === "boolean",
|
|
7
|
+
array: (v) => Array.isArray(v),
|
|
8
|
+
object: (v) => !!v && typeof v === "object" && !Array.isArray(v),
|
|
9
|
+
};
|
|
10
|
+
validateParams(params) {
|
|
11
|
+
const schema = (this.parameters ?? {});
|
|
12
|
+
if (schema.type !== "object") {
|
|
13
|
+
throw new Error(`Schema must be object type, got ${String(schema.type)}`);
|
|
14
|
+
}
|
|
15
|
+
return this.validateValue(params, { ...schema, type: "object" }, "parameter");
|
|
16
|
+
}
|
|
17
|
+
validateValue(value, schema, label) {
|
|
18
|
+
const t = schema.type;
|
|
19
|
+
const errors = [];
|
|
20
|
+
if (t && Tool.typeMap[t] && !Tool.typeMap[t](value))
|
|
21
|
+
return [`${label} should be ${t}`];
|
|
22
|
+
if (schema.enum && !(schema.enum.includes(value)))
|
|
23
|
+
errors.push(`${label} must be one of ${JSON.stringify(schema.enum)}`);
|
|
24
|
+
if (t === "integer" || t === "number") {
|
|
25
|
+
if (typeof value === "number") {
|
|
26
|
+
if (typeof schema.minimum === "number" && value < schema.minimum)
|
|
27
|
+
errors.push(`${label} must be >= ${schema.minimum}`);
|
|
28
|
+
if (typeof schema.maximum === "number" && value > schema.maximum)
|
|
29
|
+
errors.push(`${label} must be <= ${schema.maximum}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (t === "string" && typeof value === "string") {
|
|
33
|
+
if (typeof schema.minLength === "number" && value.length < schema.minLength)
|
|
34
|
+
errors.push(`${label} must be at least ${schema.minLength} chars`);
|
|
35
|
+
if (typeof schema.maxLength === "number" && value.length > schema.maxLength)
|
|
36
|
+
errors.push(`${label} must be at most ${schema.maxLength} chars`);
|
|
37
|
+
}
|
|
38
|
+
if (t === "object" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
39
|
+
const v = value;
|
|
40
|
+
const props = (schema.properties ?? {});
|
|
41
|
+
for (const req of (schema.required ?? [])) {
|
|
42
|
+
if (!(req in v))
|
|
43
|
+
errors.push(`missing required ${label === "parameter" ? req : `${label}.${req}`}`);
|
|
44
|
+
}
|
|
45
|
+
for (const [k, child] of Object.entries(v)) {
|
|
46
|
+
if (props[k])
|
|
47
|
+
errors.push(...this.validateValue(child, props[k], label === "parameter" ? k : `${label}.${k}`));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (t === "array" && Array.isArray(value) && schema.items && typeof schema.items === "object") {
|
|
51
|
+
value.forEach((item, i) => errors.push(...this.validateValue(item, schema.items, `${label}[${i}]`)));
|
|
52
|
+
}
|
|
53
|
+
return errors;
|
|
54
|
+
}
|
|
55
|
+
toSchema() {
|
|
56
|
+
return { type: "function", function: { name: this.name, description: this.description, parameters: this.parameters } };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Tool } from "./base.js";
|
|
2
|
+
import type { CronService } from "../../cron/service.js";
|
|
3
|
+
export declare class CronTool extends Tool {
|
|
4
|
+
private readonly cron;
|
|
5
|
+
readonly name = "cron";
|
|
6
|
+
readonly description = "Schedule reminders and recurring tasks. Actions: add, list, remove.";
|
|
7
|
+
readonly parameters: {
|
|
8
|
+
type: string;
|
|
9
|
+
properties: {
|
|
10
|
+
action: {
|
|
11
|
+
type: string;
|
|
12
|
+
enum: string[];
|
|
13
|
+
};
|
|
14
|
+
message: {
|
|
15
|
+
type: string;
|
|
16
|
+
};
|
|
17
|
+
every_seconds: {
|
|
18
|
+
type: string;
|
|
19
|
+
};
|
|
20
|
+
cron_expr: {
|
|
21
|
+
type: string;
|
|
22
|
+
};
|
|
23
|
+
tz: {
|
|
24
|
+
type: string;
|
|
25
|
+
};
|
|
26
|
+
at: {
|
|
27
|
+
type: string;
|
|
28
|
+
};
|
|
29
|
+
job_id: {
|
|
30
|
+
type: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
required: string[];
|
|
34
|
+
};
|
|
35
|
+
private channel;
|
|
36
|
+
private chatId;
|
|
37
|
+
constructor(cron: CronService);
|
|
38
|
+
setContext(channel: string, chatId: string): void;
|
|
39
|
+
execute(args: Record<string, unknown>): Promise<string>;
|
|
40
|
+
private addJob;
|
|
41
|
+
private listJobs;
|
|
42
|
+
private removeJob;
|
|
43
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Tool } from "./base.js";
|
|
2
|
+
export class CronTool extends Tool {
|
|
3
|
+
cron;
|
|
4
|
+
name = "cron";
|
|
5
|
+
description = "Schedule reminders and recurring tasks. Actions: add, list, remove.";
|
|
6
|
+
parameters = {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
action: { type: "string", enum: ["add", "list", "remove"] },
|
|
10
|
+
message: { type: "string" },
|
|
11
|
+
every_seconds: { type: "integer" },
|
|
12
|
+
cron_expr: { type: "string" },
|
|
13
|
+
tz: { type: "string" },
|
|
14
|
+
at: { type: "string" },
|
|
15
|
+
job_id: { type: "string" },
|
|
16
|
+
},
|
|
17
|
+
required: ["action"],
|
|
18
|
+
};
|
|
19
|
+
channel = "";
|
|
20
|
+
chatId = "";
|
|
21
|
+
constructor(cron) {
|
|
22
|
+
super();
|
|
23
|
+
this.cron = cron;
|
|
24
|
+
}
|
|
25
|
+
setContext(channel, chatId) {
|
|
26
|
+
this.channel = channel;
|
|
27
|
+
this.chatId = chatId;
|
|
28
|
+
}
|
|
29
|
+
async execute(args) {
|
|
30
|
+
const action = String(args.action ?? "");
|
|
31
|
+
if (action === "add")
|
|
32
|
+
return this.addJob(args);
|
|
33
|
+
if (action === "list")
|
|
34
|
+
return this.listJobs();
|
|
35
|
+
if (action === "remove")
|
|
36
|
+
return this.removeJob(String(args.job_id ?? ""));
|
|
37
|
+
return `Unknown action: ${action}`;
|
|
38
|
+
}
|
|
39
|
+
addJob(args) {
|
|
40
|
+
const message = String(args.message ?? "");
|
|
41
|
+
if (!message)
|
|
42
|
+
return "Error: message is required for add";
|
|
43
|
+
if (!this.channel || !this.chatId)
|
|
44
|
+
return "Error: no session context (channel/chat_id)";
|
|
45
|
+
const every = args.every_seconds != null ? Number(args.every_seconds) : null;
|
|
46
|
+
const cronExpr = args.cron_expr != null ? String(args.cron_expr) : null;
|
|
47
|
+
const tz = args.tz != null ? String(args.tz) : null;
|
|
48
|
+
const at = args.at != null ? String(args.at) : null;
|
|
49
|
+
let schedule;
|
|
50
|
+
let deleteAfterRun = false;
|
|
51
|
+
if (every)
|
|
52
|
+
schedule = { kind: "every", everyMs: every * 1000 };
|
|
53
|
+
else if (cronExpr)
|
|
54
|
+
schedule = { kind: "cron", expr: cronExpr, tz: tz ?? undefined };
|
|
55
|
+
else if (at) {
|
|
56
|
+
const dt = new Date(at);
|
|
57
|
+
if (Number.isNaN(dt.getTime()))
|
|
58
|
+
return "Error: invalid ISO datetime in at";
|
|
59
|
+
schedule = { kind: "at", atMs: dt.getTime() };
|
|
60
|
+
deleteAfterRun = true;
|
|
61
|
+
}
|
|
62
|
+
else
|
|
63
|
+
return "Error: either every_seconds, cron_expr, or at is required";
|
|
64
|
+
try {
|
|
65
|
+
const job = this.cron.addJob({ name: message.slice(0, 30), schedule, message, deliver: true, channel: this.channel, to: this.chatId, deleteAfterRun });
|
|
66
|
+
return `Created job '${job.name}' (id: ${job.id})`;
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
return `Error: ${String(err).replace(/^Error:\s*/, "")}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
listJobs() {
|
|
73
|
+
const jobs = this.cron.listJobs();
|
|
74
|
+
if (!jobs.length)
|
|
75
|
+
return "No scheduled jobs.";
|
|
76
|
+
return `Scheduled jobs:\n${jobs.map((j) => `- ${j.name} (id: ${j.id}, ${j.schedule.kind})`).join("\n")}`;
|
|
77
|
+
}
|
|
78
|
+
removeJob(id) {
|
|
79
|
+
if (!id)
|
|
80
|
+
return "Error: job_id is required for remove";
|
|
81
|
+
return this.cron.removeJob(id) ? `Removed job ${id}` : `Job ${id} not found`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Tool } from "./base.js";
|
|
2
|
+
export declare class ReadFileTool extends Tool {
|
|
3
|
+
private readonly workspace?;
|
|
4
|
+
private readonly allowedDir?;
|
|
5
|
+
readonly name = "read_file";
|
|
6
|
+
readonly description = "Read the contents of a file at the given path.";
|
|
7
|
+
readonly parameters: {
|
|
8
|
+
type: string;
|
|
9
|
+
properties: {
|
|
10
|
+
path: {
|
|
11
|
+
type: string;
|
|
12
|
+
description: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
required: string[];
|
|
16
|
+
};
|
|
17
|
+
constructor(workspace?: string | undefined, allowedDir?: string | undefined);
|
|
18
|
+
execute(args: Record<string, unknown>): Promise<string>;
|
|
19
|
+
}
|
|
20
|
+
export declare class WriteFileTool extends Tool {
|
|
21
|
+
private readonly workspace?;
|
|
22
|
+
private readonly allowedDir?;
|
|
23
|
+
readonly name = "write_file";
|
|
24
|
+
readonly description = "Write content to a file at the given path. Creates parent directories if needed.";
|
|
25
|
+
readonly parameters: {
|
|
26
|
+
type: string;
|
|
27
|
+
properties: {
|
|
28
|
+
path: {
|
|
29
|
+
type: string;
|
|
30
|
+
};
|
|
31
|
+
content: {
|
|
32
|
+
type: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
required: string[];
|
|
36
|
+
};
|
|
37
|
+
constructor(workspace?: string | undefined, allowedDir?: string | undefined);
|
|
38
|
+
execute(args: Record<string, unknown>): Promise<string>;
|
|
39
|
+
}
|
|
40
|
+
export declare class EditFileTool extends Tool {
|
|
41
|
+
private readonly workspace?;
|
|
42
|
+
private readonly allowedDir?;
|
|
43
|
+
readonly name = "edit_file";
|
|
44
|
+
readonly description = "Edit a file by replacing old_text with new_text. The old_text must exist exactly in the file.";
|
|
45
|
+
readonly parameters: {
|
|
46
|
+
type: string;
|
|
47
|
+
properties: {
|
|
48
|
+
path: {
|
|
49
|
+
type: string;
|
|
50
|
+
};
|
|
51
|
+
old_text: {
|
|
52
|
+
type: string;
|
|
53
|
+
};
|
|
54
|
+
new_text: {
|
|
55
|
+
type: string;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
required: string[];
|
|
59
|
+
};
|
|
60
|
+
constructor(workspace?: string | undefined, allowedDir?: string | undefined);
|
|
61
|
+
execute(args: Record<string, unknown>): Promise<string>;
|
|
62
|
+
}
|
|
63
|
+
export declare class ListDirTool extends Tool {
|
|
64
|
+
private readonly workspace?;
|
|
65
|
+
private readonly allowedDir?;
|
|
66
|
+
readonly name = "list_dir";
|
|
67
|
+
readonly description = "List the contents of a directory.";
|
|
68
|
+
readonly parameters: {
|
|
69
|
+
type: string;
|
|
70
|
+
properties: {
|
|
71
|
+
path: {
|
|
72
|
+
type: string;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
required: string[];
|
|
76
|
+
};
|
|
77
|
+
constructor(workspace?: string | undefined, allowedDir?: string | undefined);
|
|
78
|
+
execute(args: Record<string, unknown>): Promise<string>;
|
|
79
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Tool } from "./base.js";
|
|
4
|
+
function resolvePath(input, workspace, allowedDir) {
|
|
5
|
+
const expanded = input.startsWith("~") ? path.join(process.env.USERPROFILE || process.env.HOME || "", input.slice(1)) : input;
|
|
6
|
+
const base = path.isAbsolute(expanded) ? expanded : workspace ? path.join(workspace, expanded) : expanded;
|
|
7
|
+
const resolved = path.resolve(base);
|
|
8
|
+
if (allowedDir) {
|
|
9
|
+
const allow = path.resolve(allowedDir);
|
|
10
|
+
if (!resolved.startsWith(allow))
|
|
11
|
+
throw new Error(`Path ${input} is outside allowed directory ${allowedDir}`);
|
|
12
|
+
}
|
|
13
|
+
return resolved;
|
|
14
|
+
}
|
|
15
|
+
export class ReadFileTool extends Tool {
|
|
16
|
+
workspace;
|
|
17
|
+
allowedDir;
|
|
18
|
+
name = "read_file";
|
|
19
|
+
description = "Read the contents of a file at the given path.";
|
|
20
|
+
parameters = { type: "object", properties: { path: { type: "string", description: "The file path to read" } }, required: ["path"] };
|
|
21
|
+
constructor(workspace, allowedDir) {
|
|
22
|
+
super();
|
|
23
|
+
this.workspace = workspace;
|
|
24
|
+
this.allowedDir = allowedDir;
|
|
25
|
+
}
|
|
26
|
+
async execute(args) {
|
|
27
|
+
try {
|
|
28
|
+
const p = resolvePath(String(args.path), this.workspace, this.allowedDir);
|
|
29
|
+
if (!fs.existsSync(p))
|
|
30
|
+
return `Error: File not found: ${String(args.path)}`;
|
|
31
|
+
if (!fs.statSync(p).isFile())
|
|
32
|
+
return `Error: Not a file: ${String(args.path)}`;
|
|
33
|
+
return fs.readFileSync(p, "utf8");
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
return `Error reading file: ${String(err)}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export class WriteFileTool extends Tool {
|
|
41
|
+
workspace;
|
|
42
|
+
allowedDir;
|
|
43
|
+
name = "write_file";
|
|
44
|
+
description = "Write content to a file at the given path. Creates parent directories if needed.";
|
|
45
|
+
parameters = { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] };
|
|
46
|
+
constructor(workspace, allowedDir) {
|
|
47
|
+
super();
|
|
48
|
+
this.workspace = workspace;
|
|
49
|
+
this.allowedDir = allowedDir;
|
|
50
|
+
}
|
|
51
|
+
async execute(args) {
|
|
52
|
+
try {
|
|
53
|
+
const p = resolvePath(String(args.path), this.workspace, this.allowedDir);
|
|
54
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
55
|
+
const content = String(args.content ?? "");
|
|
56
|
+
fs.writeFileSync(p, content, "utf8");
|
|
57
|
+
return `Successfully wrote ${content.length} bytes to ${p}`;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return `Error writing file: ${String(err)}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export class EditFileTool extends Tool {
|
|
65
|
+
workspace;
|
|
66
|
+
allowedDir;
|
|
67
|
+
name = "edit_file";
|
|
68
|
+
description = "Edit a file by replacing old_text with new_text. The old_text must exist exactly in the file.";
|
|
69
|
+
parameters = { type: "object", properties: { path: { type: "string" }, old_text: { type: "string" }, new_text: { type: "string" } }, required: ["path", "old_text", "new_text"] };
|
|
70
|
+
constructor(workspace, allowedDir) {
|
|
71
|
+
super();
|
|
72
|
+
this.workspace = workspace;
|
|
73
|
+
this.allowedDir = allowedDir;
|
|
74
|
+
}
|
|
75
|
+
async execute(args) {
|
|
76
|
+
try {
|
|
77
|
+
const p = resolvePath(String(args.path), this.workspace, this.allowedDir);
|
|
78
|
+
if (!fs.existsSync(p))
|
|
79
|
+
return `Error: File not found: ${String(args.path)}`;
|
|
80
|
+
const content = fs.readFileSync(p, "utf8");
|
|
81
|
+
const oldText = String(args.old_text ?? "");
|
|
82
|
+
const newText = String(args.new_text ?? "");
|
|
83
|
+
if (!content.includes(oldText))
|
|
84
|
+
return `Error: old_text not found in ${String(args.path)}. Verify the file content.`;
|
|
85
|
+
if (content.split(oldText).length - 1 > 1)
|
|
86
|
+
return `Warning: old_text appears ${content.split(oldText).length - 1} times. Please provide more context to make it unique.`;
|
|
87
|
+
fs.writeFileSync(p, content.replace(oldText, newText), "utf8");
|
|
88
|
+
return `Successfully edited ${p}`;
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
return `Error editing file: ${String(err)}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export class ListDirTool extends Tool {
|
|
96
|
+
workspace;
|
|
97
|
+
allowedDir;
|
|
98
|
+
name = "list_dir";
|
|
99
|
+
description = "List the contents of a directory.";
|
|
100
|
+
parameters = { type: "object", properties: { path: { type: "string" } }, required: ["path"] };
|
|
101
|
+
constructor(workspace, allowedDir) {
|
|
102
|
+
super();
|
|
103
|
+
this.workspace = workspace;
|
|
104
|
+
this.allowedDir = allowedDir;
|
|
105
|
+
}
|
|
106
|
+
async execute(args) {
|
|
107
|
+
try {
|
|
108
|
+
const p = resolvePath(String(args.path), this.workspace, this.allowedDir);
|
|
109
|
+
if (!fs.existsSync(p))
|
|
110
|
+
return `Error: Directory not found: ${String(args.path)}`;
|
|
111
|
+
if (!fs.statSync(p).isDirectory())
|
|
112
|
+
return `Error: Not a directory: ${String(args.path)}`;
|
|
113
|
+
const items = fs.readdirSync(p).sort().map((n) => {
|
|
114
|
+
const full = path.join(p, n);
|
|
115
|
+
return `${fs.statSync(full).isDirectory() ? "[DIR]" : "[FILE]"} ${n}`;
|
|
116
|
+
});
|
|
117
|
+
if (!items.length)
|
|
118
|
+
return `Directory ${String(args.path)} is empty`;
|
|
119
|
+
return items.join("\n");
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
return `Error listing directory: ${String(err)}`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Tool } from "./base.js";
|
|
2
|
+
import type { OutboundMessage } from "../../bus/events.js";
|
|
3
|
+
export declare class MessageTool extends Tool {
|
|
4
|
+
private sendCallback?;
|
|
5
|
+
readonly name = "message";
|
|
6
|
+
readonly description = "Send a message to the user. Use this when you want to communicate something.";
|
|
7
|
+
readonly parameters: {
|
|
8
|
+
type: string;
|
|
9
|
+
properties: {
|
|
10
|
+
content: {
|
|
11
|
+
type: string;
|
|
12
|
+
description: string;
|
|
13
|
+
};
|
|
14
|
+
channel: {
|
|
15
|
+
type: string;
|
|
16
|
+
description: string;
|
|
17
|
+
};
|
|
18
|
+
chat_id: {
|
|
19
|
+
type: string;
|
|
20
|
+
description: string;
|
|
21
|
+
};
|
|
22
|
+
media: {
|
|
23
|
+
type: string;
|
|
24
|
+
items: {
|
|
25
|
+
type: string;
|
|
26
|
+
};
|
|
27
|
+
description: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
required: string[];
|
|
31
|
+
};
|
|
32
|
+
private defaultChannel;
|
|
33
|
+
private defaultChatId;
|
|
34
|
+
private defaultMessageId;
|
|
35
|
+
sentInTurn: boolean;
|
|
36
|
+
constructor(sendCallback?: ((msg: OutboundMessage) => Promise<void>) | undefined);
|
|
37
|
+
setContext(channel: string, chatId: string, messageId?: string): void;
|
|
38
|
+
setSendCallback(cb: (msg: OutboundMessage) => Promise<void>): void;
|
|
39
|
+
startTurn(): void;
|
|
40
|
+
execute(args: Record<string, unknown>): Promise<string>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Tool } from "./base.js";
|
|
2
|
+
export class MessageTool extends Tool {
|
|
3
|
+
sendCallback;
|
|
4
|
+
name = "message";
|
|
5
|
+
description = "Send a message to the user. Use this when you want to communicate something.";
|
|
6
|
+
parameters = {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
content: { type: "string", description: "The message content to send" },
|
|
10
|
+
channel: { type: "string", description: "Optional target channel" },
|
|
11
|
+
chat_id: { type: "string", description: "Optional target chat/user ID" },
|
|
12
|
+
media: { type: "array", items: { type: "string" }, description: "Optional file attachments" },
|
|
13
|
+
},
|
|
14
|
+
required: ["content"],
|
|
15
|
+
};
|
|
16
|
+
defaultChannel = "";
|
|
17
|
+
defaultChatId = "";
|
|
18
|
+
defaultMessageId = null;
|
|
19
|
+
sentInTurn = false;
|
|
20
|
+
constructor(sendCallback) {
|
|
21
|
+
super();
|
|
22
|
+
this.sendCallback = sendCallback;
|
|
23
|
+
}
|
|
24
|
+
setContext(channel, chatId, messageId) {
|
|
25
|
+
this.defaultChannel = channel;
|
|
26
|
+
this.defaultChatId = chatId;
|
|
27
|
+
this.defaultMessageId = messageId ?? null;
|
|
28
|
+
}
|
|
29
|
+
setSendCallback(cb) {
|
|
30
|
+
this.sendCallback = cb;
|
|
31
|
+
}
|
|
32
|
+
startTurn() {
|
|
33
|
+
this.sentInTurn = false;
|
|
34
|
+
}
|
|
35
|
+
async execute(args) {
|
|
36
|
+
const content = String(args.content ?? "");
|
|
37
|
+
const channel = String(args.channel ?? this.defaultChannel);
|
|
38
|
+
const chatId = String(args.chat_id ?? this.defaultChatId);
|
|
39
|
+
const messageId = String(args.message_id ?? this.defaultMessageId ?? "");
|
|
40
|
+
const media = Array.isArray(args.media) ? args.media : [];
|
|
41
|
+
if (!channel || !chatId)
|
|
42
|
+
return "Error: No target channel/chat specified";
|
|
43
|
+
if (!this.sendCallback)
|
|
44
|
+
return "Error: Message sending not configured";
|
|
45
|
+
try {
|
|
46
|
+
await this.sendCallback({ channel, chatId, content, media, metadata: { message_id: messageId } });
|
|
47
|
+
if (channel === this.defaultChannel && chatId === this.defaultChatId)
|
|
48
|
+
this.sentInTurn = true;
|
|
49
|
+
return `Message sent to ${channel}:${chatId}${media.length ? ` with ${media.length} attachments` : ""}`;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
return `Error sending message: ${String(err)}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Tool } from "./base.js";
|
|
2
|
+
export declare class ToolRegistry {
|
|
3
|
+
private tools;
|
|
4
|
+
register(tool: Tool): void;
|
|
5
|
+
get(name: string): Tool | undefined;
|
|
6
|
+
getDefinitions(): Array<Record<string, unknown>>;
|
|
7
|
+
get toolNames(): string[];
|
|
8
|
+
execute(name: string, params: Record<string, unknown>): Promise<string>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export class ToolRegistry {
|
|
2
|
+
tools = new Map();
|
|
3
|
+
register(tool) {
|
|
4
|
+
this.tools.set(tool.name, tool);
|
|
5
|
+
}
|
|
6
|
+
get(name) {
|
|
7
|
+
return this.tools.get(name);
|
|
8
|
+
}
|
|
9
|
+
getDefinitions() {
|
|
10
|
+
return [...this.tools.values()].map((t) => t.toSchema());
|
|
11
|
+
}
|
|
12
|
+
get toolNames() {
|
|
13
|
+
return [...this.tools.keys()];
|
|
14
|
+
}
|
|
15
|
+
async execute(name, params) {
|
|
16
|
+
const hint = "\n\n[Analyze the error above and try a different approach.]";
|
|
17
|
+
const tool = this.tools.get(name);
|
|
18
|
+
if (!tool)
|
|
19
|
+
return `Error: Tool '${name}' not found. Available: ${this.toolNames.join(", ")}`;
|
|
20
|
+
try {
|
|
21
|
+
const errors = tool.validateParams(params);
|
|
22
|
+
if (errors.length)
|
|
23
|
+
return `Error: Invalid parameters for tool '${name}': ${errors.join("; ")}${hint}`;
|
|
24
|
+
const result = await tool.execute(params);
|
|
25
|
+
if (result.startsWith("Error"))
|
|
26
|
+
return result + hint;
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
return `Error executing ${name}: ${String(err)}${hint}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Tool } from "./base.js";
|
|
2
|
+
export declare class ExecTool extends Tool {
|
|
3
|
+
private readonly timeout;
|
|
4
|
+
private readonly workingDir?;
|
|
5
|
+
private readonly restrictToWorkspace;
|
|
6
|
+
private readonly pathAppend;
|
|
7
|
+
readonly name = "exec";
|
|
8
|
+
readonly description = "Execute a shell command and return its output. Use with caution.";
|
|
9
|
+
readonly parameters: {
|
|
10
|
+
type: string;
|
|
11
|
+
properties: {
|
|
12
|
+
command: {
|
|
13
|
+
type: string;
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
working_dir: {
|
|
17
|
+
type: string;
|
|
18
|
+
description: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
required: string[];
|
|
22
|
+
};
|
|
23
|
+
constructor(timeout?: number, workingDir?: string | undefined, restrictToWorkspace?: boolean, pathAppend?: string);
|
|
24
|
+
private guard;
|
|
25
|
+
execute(args: Record<string, unknown>): Promise<string>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { Tool } from "./base.js";
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
export class ExecTool extends Tool {
|
|
7
|
+
timeout;
|
|
8
|
+
workingDir;
|
|
9
|
+
restrictToWorkspace;
|
|
10
|
+
pathAppend;
|
|
11
|
+
name = "exec";
|
|
12
|
+
description = "Execute a shell command and return its output. Use with caution.";
|
|
13
|
+
parameters = {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
command: { type: "string", description: "The shell command to execute" },
|
|
17
|
+
working_dir: { type: "string", description: "Optional working directory for the command" },
|
|
18
|
+
},
|
|
19
|
+
required: ["command"],
|
|
20
|
+
};
|
|
21
|
+
constructor(timeout = 60, workingDir, restrictToWorkspace = false, pathAppend = "") {
|
|
22
|
+
super();
|
|
23
|
+
this.timeout = timeout;
|
|
24
|
+
this.workingDir = workingDir;
|
|
25
|
+
this.restrictToWorkspace = restrictToWorkspace;
|
|
26
|
+
this.pathAppend = pathAppend;
|
|
27
|
+
}
|
|
28
|
+
guard(command, cwd) {
|
|
29
|
+
const lower = command.toLowerCase();
|
|
30
|
+
const deny = [/\brm\s+-[rf]{1,2}\b/, /\bdel\s+\/[fq]\b/, /\brmdir\s+\/s\b/, /(?:^|[;&|]\s*)format\b/, /\b(mkfs|diskpart)\b/, /\bdd\s+if=/, />\s*\/dev\/sd/, /\b(shutdown|reboot|poweroff)\b/, /:\(\)\s*\{.*\};\s*:/];
|
|
31
|
+
if (deny.some((r) => r.test(lower)))
|
|
32
|
+
return "Error: Command blocked by safety guard (dangerous pattern detected)";
|
|
33
|
+
if (this.restrictToWorkspace) {
|
|
34
|
+
if (command.includes("../") || command.includes("..\\"))
|
|
35
|
+
return "Error: Command blocked by safety guard (path traversal detected)";
|
|
36
|
+
const abs = command.match(/[A-Za-z]:\\[^\s"']+/g) ?? [];
|
|
37
|
+
for (const raw of abs) {
|
|
38
|
+
const p = path.resolve(raw);
|
|
39
|
+
const c = path.resolve(cwd);
|
|
40
|
+
if (!p.startsWith(c))
|
|
41
|
+
return "Error: Command blocked by safety guard (path outside working dir)";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
async execute(args) {
|
|
47
|
+
const command = String(args.command ?? "");
|
|
48
|
+
const cwd = String(args.working_dir ?? this.workingDir ?? process.cwd());
|
|
49
|
+
const blocked = this.guard(command, cwd);
|
|
50
|
+
if (blocked)
|
|
51
|
+
return blocked;
|
|
52
|
+
const env = { ...process.env };
|
|
53
|
+
if (this.pathAppend)
|
|
54
|
+
env.PATH = `${env.PATH ?? ""}${path.delimiter}${this.pathAppend}`;
|
|
55
|
+
try {
|
|
56
|
+
const { stdout, stderr } = await execAsync(command, { cwd, env, timeout: this.timeout * 1000, maxBuffer: 1024 * 1024 });
|
|
57
|
+
let out = "";
|
|
58
|
+
if (stdout)
|
|
59
|
+
out += stdout;
|
|
60
|
+
if (stderr?.trim())
|
|
61
|
+
out += `${out ? "\n" : ""}STDERR:\n${stderr}`;
|
|
62
|
+
if (!out)
|
|
63
|
+
out = "(no output)";
|
|
64
|
+
if (out.length > 10000)
|
|
65
|
+
out = `${out.slice(0, 10000)}\n... (truncated, ${out.length - 10000} more chars)`;
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
if (typeof err?.killed === "boolean" && err.killed)
|
|
70
|
+
return `Error: Command timed out after ${this.timeout} seconds`;
|
|
71
|
+
const stdout = err?.stdout ? `${err.stdout}\n` : "";
|
|
72
|
+
const stderr = err?.stderr ? `STDERR:\n${err.stderr}\n` : "";
|
|
73
|
+
const code = typeof err?.code === "number" ? `\nExit code: ${err.code}` : "";
|
|
74
|
+
const text = `${stdout}${stderr}${code}`.trim();
|
|
75
|
+
return text || `Error executing command: ${String(err)}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Tool } from "./base.js";
|
|
2
|
+
import type { SubagentManager } from "../subagent.js";
|
|
3
|
+
export declare class SpawnTool extends Tool {
|
|
4
|
+
private readonly manager;
|
|
5
|
+
readonly name = "spawn";
|
|
6
|
+
readonly description = "Spawn a subagent to handle a task in the background.";
|
|
7
|
+
readonly parameters: {
|
|
8
|
+
type: string;
|
|
9
|
+
properties: {
|
|
10
|
+
task: {
|
|
11
|
+
type: string;
|
|
12
|
+
description: string;
|
|
13
|
+
};
|
|
14
|
+
label: {
|
|
15
|
+
type: string;
|
|
16
|
+
description: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
required: string[];
|
|
20
|
+
};
|
|
21
|
+
private originChannel;
|
|
22
|
+
private originChatId;
|
|
23
|
+
private sessionKey;
|
|
24
|
+
constructor(manager: SubagentManager);
|
|
25
|
+
setContext(channel: string, chatId: string): void;
|
|
26
|
+
execute(args: Record<string, unknown>): Promise<string>;
|
|
27
|
+
}
|