branch:
tools.ts
9371 bytesRaw
import { tool } from "ai";
import { z } from "zod";
/** Create the SQLite tables (idempotent). */
export function initDatabase(sql: SqlStorage) {
sql.exec(`CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now'))
)`);
sql.exec(`CREATE TABLE IF NOT EXISTS sprints (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
name TEXT NOT NULL,
start_date TEXT,
end_date TEXT,
status TEXT DEFAULT 'planned',
created_at TEXT DEFAULT (datetime('now'))
)`);
sql.exec(`CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT DEFAULT '',
status TEXT DEFAULT 'todo',
priority TEXT DEFAULT 'medium',
assignee TEXT DEFAULT '',
sprint_id TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
)`);
sql.exec(`CREATE TABLE IF NOT EXISTS comments (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
author TEXT DEFAULT 'user',
content TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
)`);
}
/** Build the PM tools wired to the given SqlStorage using AI SDK tool(). */
export function createTools(sql: SqlStorage) {
return {
createProject: tool({
description: "Create a new project",
inputSchema: z.object({
name: z.string().describe("Project name"),
description: z.string().optional().describe("Project description")
}),
execute: async ({ name, description }) => {
const id = crypto.randomUUID();
sql.exec(
"INSERT INTO projects (id, name, description) VALUES (?, ?, ?)",
id,
name,
description ?? ""
);
return { id, name, description: description ?? "" };
}
}),
listProjects: tool({
description: "List all projects",
inputSchema: z.object({}),
execute: async () => {
return sql
.exec("SELECT * FROM projects ORDER BY created_at DESC")
.toArray();
}
}),
createTask: tool({
description: "Create a task in a project",
inputSchema: z.object({
projectId: z.string().describe("Project ID"),
title: z.string().describe("Task title"),
description: z.string().optional().describe("Task description"),
status: z
.enum(["todo", "in_progress", "in_review", "done"])
.optional()
.describe("Task status"),
priority: z
.enum(["low", "medium", "high", "urgent"])
.optional()
.describe("Priority level"),
assignee: z.string().optional().describe("Assignee name"),
sprintId: z.string().optional().describe("Sprint ID")
}),
execute: async ({
projectId,
title,
description,
status,
priority,
assignee,
sprintId
}) => {
const id = crypto.randomUUID();
sql.exec(
`INSERT INTO tasks (id, project_id, title, description, status, priority, assignee, sprint_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
id,
projectId,
title,
description ?? "",
status ?? "todo",
priority ?? "medium",
assignee ?? "",
sprintId ?? null
);
return {
id,
projectId,
title,
status: status ?? "todo",
priority: priority ?? "medium"
};
}
}),
listTasks: tool({
description: "List tasks with optional filters",
inputSchema: z.object({
projectId: z.string().optional().describe("Filter by project ID"),
status: z.string().optional().describe("Filter by status"),
priority: z.string().optional().describe("Filter by priority"),
assignee: z.string().optional().describe("Filter by assignee"),
sprintId: z.string().optional().describe("Filter by sprint ID")
}),
execute: async ({ projectId, status, priority, assignee, sprintId }) => {
let query = "SELECT * FROM tasks WHERE 1=1";
const params: unknown[] = [];
if (projectId) {
query += " AND project_id = ?";
params.push(projectId);
}
if (status) {
query += " AND status = ?";
params.push(status);
}
if (priority) {
query += " AND priority = ?";
params.push(priority);
}
if (assignee) {
query += " AND assignee = ?";
params.push(assignee);
}
if (sprintId) {
query += " AND sprint_id = ?";
params.push(sprintId);
}
query += " ORDER BY created_at DESC";
return sql.exec(query, ...params).toArray();
}
}),
updateTask: tool({
description: "Update a task's fields",
inputSchema: z.object({
id: z.string().describe("Task ID"),
title: z.string().optional().describe("New title"),
description: z.string().optional().describe("New description"),
status: z
.enum(["todo", "in_progress", "in_review", "done"])
.optional()
.describe("New status"),
priority: z
.enum(["low", "medium", "high", "urgent"])
.optional()
.describe("New priority"),
assignee: z.string().optional().describe("New assignee"),
sprintId: z.string().optional().describe("New sprint ID")
}),
execute: async ({ id, ...fields }) => {
const fieldToColumn: Record<string, string> = {
title: "title",
description: "description",
status: "status",
priority: "priority",
assignee: "assignee",
sprintId: "sprint_id"
};
const sets: string[] = [];
const params: unknown[] = [];
for (const [key, value] of Object.entries(fields)) {
const col = fieldToColumn[key];
if (col && value !== undefined) {
sets.push(`${col} = ?`);
params.push(value);
}
}
if (sets.length === 0) return { error: "No fields to update" };
sets.push("updated_at = datetime('now')");
params.push(id);
sql.exec(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, ...params);
return (
sql.exec("SELECT * FROM tasks WHERE id = ?", id).toArray()[0] ?? {
error: "Task not found"
}
);
}
}),
deleteTask: tool({
description: "Delete a task and its comments",
inputSchema: z.object({
id: z.string().describe("Task ID to delete")
}),
execute: async ({ id }) => {
sql.exec("DELETE FROM comments WHERE task_id = ?", id);
sql.exec("DELETE FROM tasks WHERE id = ?", id);
return { deleted: id };
}
}),
createSprint: tool({
description: "Create a sprint for a project",
inputSchema: z.object({
projectId: z.string().describe("Project ID"),
name: z.string().describe("Sprint name"),
startDate: z.string().optional().describe("Start date (ISO 8601)"),
endDate: z.string().optional().describe("End date (ISO 8601)")
}),
execute: async ({ projectId, name, startDate, endDate }) => {
const id = crypto.randomUUID();
sql.exec(
"INSERT INTO sprints (id, project_id, name, start_date, end_date) VALUES (?, ?, ?, ?, ?)",
id,
projectId,
name,
startDate ?? null,
endDate ?? null
);
return { id, projectId, name, startDate, endDate, status: "planned" };
}
}),
listSprints: tool({
description: "List sprints, optionally by project",
inputSchema: z.object({
projectId: z.string().optional().describe("Filter by project ID")
}),
execute: async ({ projectId }) => {
if (projectId) {
return sql
.exec(
"SELECT * FROM sprints WHERE project_id = ? ORDER BY created_at DESC",
projectId
)
.toArray();
}
return sql
.exec("SELECT * FROM sprints ORDER BY created_at DESC")
.toArray();
}
}),
addComment: tool({
description: "Add a comment to a task",
inputSchema: z.object({
taskId: z.string().describe("Task ID"),
content: z.string().describe("Comment content"),
author: z.string().optional().describe("Author name")
}),
execute: async ({ taskId, content, author }) => {
const id = crypto.randomUUID();
sql.exec(
"INSERT INTO comments (id, task_id, author, content) VALUES (?, ?, ?, ?)",
id,
taskId,
author ?? "user",
content
);
return { id, taskId, author: author ?? "user", content };
}
}),
listComments: tool({
description: "List comments on a task",
inputSchema: z.object({
taskId: z.string().describe("Task ID")
}),
execute: async ({ taskId }) => {
return sql
.exec(
"SELECT * FROM comments WHERE task_id = ? ORDER BY created_at ASC",
taskId
)
.toArray();
}
})
};
}