production: reminder cron, dashboard overhaul, shadcn components, setup wizard
- /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
This commit is contained in:
194
lib/tools.ts
Normal file
194
lib/tools.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
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}` };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user