Files
calvana/lib/tools.ts
Omair Saleh c79b9bcabc 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
2026-03-03 05:11:17 +08:00

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}` };
}
}