- /api/cron/reminders: processes pending reminders every 15min, sends WhatsApp with email fallback - /api/cron/overdue: marks overdue pledges daily (7d deferred, 14d immediate) - /api/pledges: GET handler with filtering, search, pagination, sort by dueDate - Dashboard overview: stats, collection progress bar, needs attention, upcoming payments - Dashboard pledges: proper table with status tabs, search, actions, pagination - New shadcn components: Table, Tabs, DropdownMenu, Progress - Setup wizard: 4-step onboarding (org → bank → event → QR code) - Settings API: PUT handler for org create/update - Org resolver: single-tenant fallback to first org - Cron jobs installed: reminders every 15min, overdue check at 6am - Auto-generates installment dates when not provided - HOSTNAME=0.0.0.0 in compose for multi-network binding
195 lines
5.4 KiB
TypeScript
195 lines
5.4 KiB
TypeScript
import { execSync } from "child_process";
|
|
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
import { dirname } from "path";
|
|
|
|
export interface ToolDefinition {
|
|
name: string;
|
|
description: string;
|
|
input_schema: Record<string, unknown>;
|
|
}
|
|
|
|
export const toolDefs: ToolDefinition[] = [
|
|
{
|
|
name: "bash",
|
|
description:
|
|
"Execute a bash command. Returns stdout and stderr. Use for running commands, installing packages, git, etc. Timeout: 120s.",
|
|
input_schema: {
|
|
type: "object",
|
|
properties: {
|
|
command: {
|
|
type: "string",
|
|
description: "The bash command to execute",
|
|
},
|
|
},
|
|
required: ["command"],
|
|
},
|
|
},
|
|
{
|
|
name: "read_file",
|
|
description:
|
|
"Read the contents of a file. Returns the text content. Use for examining code, configs, etc.",
|
|
input_schema: {
|
|
type: "object",
|
|
properties: {
|
|
path: { type: "string", description: "Path to the file" },
|
|
limit: {
|
|
type: "number",
|
|
description: "Max lines to read (default: all)",
|
|
},
|
|
offset: {
|
|
type: "number",
|
|
description: "Line to start from, 1-indexed (default: 1)",
|
|
},
|
|
},
|
|
required: ["path"],
|
|
},
|
|
},
|
|
{
|
|
name: "write_file",
|
|
description:
|
|
"Write content to a file. Creates parent directories if needed. Overwrites existing files.",
|
|
input_schema: {
|
|
type: "object",
|
|
properties: {
|
|
path: { type: "string", description: "Path to the file" },
|
|
content: {
|
|
type: "string",
|
|
description: "Content to write",
|
|
},
|
|
},
|
|
required: ["path", "content"],
|
|
},
|
|
},
|
|
{
|
|
name: "edit_file",
|
|
description:
|
|
"Edit a file by replacing exact text. oldText must match exactly including whitespace.",
|
|
input_schema: {
|
|
type: "object",
|
|
properties: {
|
|
path: { type: "string", description: "Path to the file" },
|
|
old_text: {
|
|
type: "string",
|
|
description: "Exact text to find",
|
|
},
|
|
new_text: {
|
|
type: "string",
|
|
description: "Replacement text",
|
|
},
|
|
},
|
|
required: ["path", "old_text", "new_text"],
|
|
},
|
|
},
|
|
{
|
|
name: "done",
|
|
description:
|
|
"Call this when the task is fully complete. Provide a short summary of what was accomplished.",
|
|
input_schema: {
|
|
type: "object",
|
|
properties: {
|
|
summary: {
|
|
type: "string",
|
|
description: "Short summary of what was done",
|
|
},
|
|
},
|
|
required: ["summary"],
|
|
},
|
|
},
|
|
{
|
|
name: "ask_user",
|
|
description:
|
|
"Ask the user a question when you need clarification or a decision. The user will respond via Telegram.",
|
|
input_schema: {
|
|
type: "object",
|
|
properties: {
|
|
question: {
|
|
type: "string",
|
|
description: "The question to ask",
|
|
},
|
|
},
|
|
required: ["question"],
|
|
},
|
|
},
|
|
];
|
|
|
|
export function executeTool(
|
|
name: string,
|
|
input: Record<string, unknown>
|
|
): { result: string; isDone?: boolean; isQuestion?: boolean } {
|
|
try {
|
|
switch (name) {
|
|
case "bash": {
|
|
const cmd = input.command as string;
|
|
try {
|
|
const output = execSync(cmd, {
|
|
encoding: "utf-8",
|
|
timeout: 120_000,
|
|
maxBuffer: 1024 * 1024,
|
|
cwd: process.cwd(),
|
|
});
|
|
const trimmed = output.length > 10000
|
|
? output.slice(0, 10000) + "\n...(truncated)"
|
|
: output;
|
|
return { result: trimmed || "(no output)" };
|
|
} catch (e: any) {
|
|
const stderr = e.stderr || "";
|
|
const stdout = e.stdout || "";
|
|
return {
|
|
result: `Exit code: ${e.status}\nSTDOUT: ${stdout}\nSTDERR: ${stderr}`.slice(
|
|
0,
|
|
5000
|
|
),
|
|
};
|
|
}
|
|
}
|
|
|
|
case "read_file": {
|
|
const path = input.path as string;
|
|
const content = readFileSync(path, "utf-8");
|
|
const lines = content.split("\n");
|
|
const offset = ((input.offset as number) || 1) - 1;
|
|
const limit = (input.limit as number) || lines.length;
|
|
const slice = lines.slice(offset, offset + limit).join("\n");
|
|
return {
|
|
result:
|
|
slice.length > 10000
|
|
? slice.slice(0, 10000) + "\n...(truncated)"
|
|
: slice,
|
|
};
|
|
}
|
|
|
|
case "write_file": {
|
|
const path = input.path as string;
|
|
mkdirSync(dirname(path), { recursive: true });
|
|
writeFileSync(path, input.content as string, "utf-8");
|
|
return { result: `Written ${(input.content as string).length} bytes to ${path}` };
|
|
}
|
|
|
|
case "edit_file": {
|
|
const path = input.path as string;
|
|
const content = readFileSync(path, "utf-8");
|
|
const oldText = input.old_text as string;
|
|
const newText = input.new_text as string;
|
|
if (!content.includes(oldText)) {
|
|
return { result: `ERROR: old_text not found in ${path}` };
|
|
}
|
|
writeFileSync(path, content.replace(oldText, newText), "utf-8");
|
|
return { result: `Edited ${path}` };
|
|
}
|
|
|
|
case "done": {
|
|
return { result: input.summary as string, isDone: true };
|
|
}
|
|
|
|
case "ask_user": {
|
|
return { result: input.question as string, isQuestion: true };
|
|
}
|
|
|
|
default:
|
|
return { result: `Unknown tool: ${name}` };
|
|
}
|
|
} catch (e: any) {
|
|
return { result: `Tool error: ${e.message}` };
|
|
}
|
|
}
|