723 lines
26 KiB
Rust
723 lines
26 KiB
Rust
//! Tools: project_list_boards, project_create_board, project_update_board,
|
|
//! project_create_board_card, project_update_board_card, project_delete_board_card
|
|
|
|
use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema};
|
|
use chrono::Utc;
|
|
use models::projects::{
|
|
project_board, project_board_card, project_board_column, project_members,
|
|
};
|
|
use models::projects::{MemberRole, ProjectBoard, ProjectBoardCard, ProjectBoardColumn};
|
|
use models::users::user::Model as UserModel;
|
|
use sea_orm::*;
|
|
use std::collections::HashMap;
|
|
use uuid::Uuid;
|
|
|
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
/// Check if the sender is an admin or owner of the project.
|
|
async fn require_admin(
|
|
db: &impl ConnectionTrait,
|
|
project_id: Uuid,
|
|
sender_id: Uuid,
|
|
) -> Result<(), ToolError> {
|
|
let member = project_members::Entity::find()
|
|
.filter(project_members::Column::Project.eq(project_id))
|
|
.filter(project_members::Column::User.eq(sender_id))
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
|
|
|
let member = member
|
|
.ok_or_else(|| ToolError::ExecutionError("You are not a member of this project".into()))?;
|
|
let role = member
|
|
.scope_role()
|
|
.map_err(|_| ToolError::ExecutionError("Unknown member role".into()))?;
|
|
|
|
match role {
|
|
MemberRole::Admin | MemberRole::Owner => Ok(()),
|
|
MemberRole::Member => Err(ToolError::ExecutionError(
|
|
"Only admin or owner can perform this action".into(),
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn serde_user(u: &UserModel) -> serde_json::Value {
|
|
serde_json::json!({
|
|
"id": u.uid.to_string(),
|
|
"username": u.username,
|
|
"display_name": u.display_name,
|
|
})
|
|
}
|
|
|
|
// ─── list boards ──────────────────────────────────────────────────────────────
|
|
|
|
pub async fn list_boards_exec(
|
|
ctx: ToolContext,
|
|
_args: serde_json::Value,
|
|
) -> Result<serde_json::Value, ToolError> {
|
|
let project_id = ctx.project_id();
|
|
let db = ctx.db();
|
|
|
|
let boards = project_board::Entity::find()
|
|
.filter(project_board::Column::Project.eq(project_id))
|
|
.order_by_asc(project_board::Column::CreatedAt)
|
|
.all(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
|
|
|
let board_ids: Vec<_> = boards.iter().map(|b| b.id).collect();
|
|
|
|
// Batch-load columns and cards
|
|
let columns = project_board_column::Entity::find()
|
|
.filter(project_board_column::Column::Board.is_in(board_ids.clone()))
|
|
.order_by_asc(project_board_column::Column::Position)
|
|
.all(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
|
|
|
let column_ids: Vec<_> = columns.iter().map(|c| c.id).collect();
|
|
let cards = project_board_card::Entity::find()
|
|
.filter(project_board_card::Column::Column.is_in(column_ids.clone()))
|
|
.order_by_asc(project_board_card::Column::Position)
|
|
.all(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
|
|
|
// Build column map
|
|
let col_map: std::collections::HashMap<Uuid, Vec<serde_json::Value>> = columns
|
|
.clone()
|
|
.into_iter()
|
|
.map(|c| {
|
|
let cards_in_col: Vec<serde_json::Value> = cards
|
|
.iter()
|
|
.filter(|card| card.column == c.id)
|
|
.map(|card| {
|
|
serde_json::json!({
|
|
"id": card.id.to_string(),
|
|
"issue_id": card.issue_id,
|
|
"title": card.title,
|
|
"description": card.description,
|
|
"position": card.position,
|
|
"assignee_id": card.assignee_id.map(|id| id.to_string()),
|
|
"due_date": card.due_date.map(|t| t.to_rfc3339()),
|
|
"priority": card.priority,
|
|
"created_at": card.created_at.to_rfc3339(),
|
|
})
|
|
})
|
|
.collect();
|
|
(c.id, cards_in_col)
|
|
})
|
|
.collect();
|
|
|
|
// Build column list per board
|
|
let mut board_col_map: std::collections::HashMap<Uuid, Vec<serde_json::Value>> = columns
|
|
.into_iter()
|
|
.fold(std::collections::HashMap::new(), |mut acc, c| {
|
|
let cards = col_map.get(&c.id).cloned().unwrap_or_default();
|
|
acc.entry(c.board).or_default().push(serde_json::json!({
|
|
"id": c.id.to_string(),
|
|
"name": c.name,
|
|
"position": c.position,
|
|
"wip_limit": c.wip_limit,
|
|
"color": c.color,
|
|
"cards": cards,
|
|
}));
|
|
acc
|
|
});
|
|
|
|
let result: Vec<_> = boards
|
|
.into_iter()
|
|
.map(|b| {
|
|
let cols = board_col_map.remove(&b.id).unwrap_or_default();
|
|
serde_json::json!({
|
|
"id": b.id.to_string(),
|
|
"name": b.name,
|
|
"description": b.description,
|
|
"created_by": b.created_by.to_string(),
|
|
"created_at": b.created_at.to_rfc3339(),
|
|
"updated_at": b.updated_at.to_rfc3339(),
|
|
"columns": cols,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
Ok(serde_json::to_value(result).map_err(|e| ToolError::ExecutionError(e.to_string()))?)
|
|
}
|
|
|
|
// ─── create board ─────────────────────────────────────────────────────────────
|
|
|
|
pub async fn create_board_exec(
|
|
ctx: ToolContext,
|
|
args: serde_json::Value,
|
|
) -> Result<serde_json::Value, ToolError> {
|
|
let project_id = ctx.project_id();
|
|
let sender_id = ctx
|
|
.sender_id()
|
|
.ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?;
|
|
let db = ctx.db();
|
|
|
|
require_admin(db, project_id, sender_id).await?;
|
|
|
|
let name = args
|
|
.get("name")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or_else(|| ToolError::ExecutionError("name is required".into()))?
|
|
.to_string();
|
|
|
|
let description = args
|
|
.get("description")
|
|
.and_then(|v| v.as_str())
|
|
.map(|s| s.to_string());
|
|
|
|
let now = Utc::now();
|
|
let active = project_board::ActiveModel {
|
|
id: Set(Uuid::now_v7()),
|
|
project: Set(project_id),
|
|
name: Set(name.clone()),
|
|
description: Set(description),
|
|
created_by: Set(sender_id),
|
|
created_at: Set(now),
|
|
updated_at: Set(now),
|
|
};
|
|
|
|
let model = active
|
|
.insert(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
|
|
|
Ok(serde_json::json!({
|
|
"id": model.id.to_string(),
|
|
"name": model.name,
|
|
"description": model.description,
|
|
"created_by": model.created_by.to_string(),
|
|
"created_at": model.created_at.to_rfc3339(),
|
|
"updated_at": model.updated_at.to_rfc3339(),
|
|
"columns": Vec::<serde_json::Value>::new(),
|
|
}))
|
|
}
|
|
|
|
// ─── update board ─────────────────────────────────────────────────────────────
|
|
|
|
pub async fn update_board_exec(
|
|
ctx: ToolContext,
|
|
args: serde_json::Value,
|
|
) -> Result<serde_json::Value, ToolError> {
|
|
let project_id = ctx.project_id();
|
|
let sender_id = ctx
|
|
.sender_id()
|
|
.ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?;
|
|
let db = ctx.db();
|
|
|
|
require_admin(db, project_id, sender_id).await?;
|
|
|
|
let board_id = args
|
|
.get("board_id")
|
|
.and_then(|v| Uuid::parse_str(v.as_str()?).ok())
|
|
.ok_or_else(|| ToolError::ExecutionError("board_id is required".into()))?;
|
|
|
|
let board = ProjectBoard::find_by_id(board_id)
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
|
|
.ok_or_else(|| ToolError::ExecutionError("Board not found".into()))?;
|
|
|
|
if board.project != project_id {
|
|
return Err(ToolError::ExecutionError(
|
|
"Board does not belong to this project".into(),
|
|
));
|
|
}
|
|
|
|
let mut active: project_board::ActiveModel = board.clone().into();
|
|
let mut updated = false;
|
|
|
|
if let Some(name) = args.get("name").and_then(|v| v.as_str()) {
|
|
active.name = Set(name.to_string());
|
|
updated = true;
|
|
}
|
|
if let Some(description) = args.get("description") {
|
|
active.description = Set(description.as_str().map(|s| s.to_string()));
|
|
updated = true;
|
|
}
|
|
|
|
if !updated {
|
|
return Err(ToolError::ExecutionError(
|
|
"At least one field must be provided".into(),
|
|
));
|
|
}
|
|
|
|
active.updated_at = Set(Utc::now());
|
|
let model = active
|
|
.update(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
|
|
|
Ok(serde_json::json!({
|
|
"id": model.id.to_string(),
|
|
"name": model.name,
|
|
"description": model.description,
|
|
"created_by": model.created_by.to_string(),
|
|
"created_at": model.created_at.to_rfc3339(),
|
|
"updated_at": model.updated_at.to_rfc3339(),
|
|
}))
|
|
}
|
|
|
|
// ─── create board card ───────────────────────────────────────────────────────
|
|
|
|
pub async fn create_board_card_exec(
|
|
ctx: ToolContext,
|
|
args: serde_json::Value,
|
|
) -> Result<serde_json::Value, ToolError> {
|
|
let project_id = ctx.project_id();
|
|
let sender_id = ctx
|
|
.sender_id()
|
|
.ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?;
|
|
let db = ctx.db();
|
|
|
|
let board_id = args
|
|
.get("board_id")
|
|
.and_then(|v| Uuid::parse_str(v.as_str()?).ok())
|
|
.ok_or_else(|| ToolError::ExecutionError("board_id is required".into()))?;
|
|
|
|
let title = args
|
|
.get("title")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or_else(|| ToolError::ExecutionError("title is required".into()))?
|
|
.to_string();
|
|
|
|
let column_id = args
|
|
.get("column_id")
|
|
.and_then(|v| Uuid::parse_str(v.as_str()?).ok());
|
|
|
|
let description = args
|
|
.get("description")
|
|
.and_then(|v| v.as_str())
|
|
.map(|s| s.to_string());
|
|
|
|
let priority = args
|
|
.get("priority")
|
|
.and_then(|v| v.as_str())
|
|
.map(|s| s.to_string());
|
|
|
|
let assignee_id = args
|
|
.get("assignee_id")
|
|
.and_then(|v| Uuid::parse_str(v.as_str()?).ok());
|
|
|
|
// Verify board belongs to project
|
|
let board = ProjectBoard::find_by_id(board_id)
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
|
|
.ok_or_else(|| ToolError::ExecutionError("Board not found".into()))?;
|
|
|
|
if board.project != project_id {
|
|
return Err(ToolError::ExecutionError(
|
|
"Board does not belong to this project".into(),
|
|
));
|
|
}
|
|
|
|
// Get target column (first column if not specified)
|
|
let target_column = if let Some(col_id) = column_id {
|
|
let col = ProjectBoardColumn::find_by_id(col_id)
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
|
|
.ok_or_else(|| ToolError::ExecutionError("Column not found".into()))?;
|
|
if col.board != board_id {
|
|
return Err(ToolError::ExecutionError(
|
|
"Column does not belong to this board".into(),
|
|
));
|
|
}
|
|
col
|
|
} else {
|
|
ProjectBoardColumn::find()
|
|
.filter(project_board_column::Column::Board.eq(board_id))
|
|
.order_by_asc(project_board_column::Column::Position)
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
|
|
.ok_or_else(|| ToolError::ExecutionError("No columns found in this board".into()))?
|
|
};
|
|
|
|
// Next position
|
|
let max_pos: Option<Option<i32>> = ProjectBoardCard::find()
|
|
.filter(project_board_card::Column::Column.eq(target_column.id))
|
|
.select_only()
|
|
.column_as(project_board_card::Column::Position.max(), "max_pos")
|
|
.into_tuple::<Option<i32>>()
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
|
let position = max_pos.flatten().unwrap_or(0) + 1;
|
|
|
|
let now = Utc::now();
|
|
let active = project_board_card::ActiveModel {
|
|
id: Set(Uuid::now_v7()),
|
|
column: Set(target_column.id),
|
|
issue_id: Set(None),
|
|
project: Set(Some(project_id)),
|
|
title: Set(title),
|
|
description: Set(description),
|
|
position: Set(position),
|
|
assignee_id: Set(assignee_id),
|
|
due_date: Set(None),
|
|
priority: Set(priority),
|
|
created_by: Set(sender_id),
|
|
created_at: Set(now),
|
|
updated_at: Set(now),
|
|
};
|
|
|
|
let model = active
|
|
.insert(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
|
|
|
Ok(serde_json::json!({
|
|
"id": model.id.to_string(),
|
|
"column_id": model.column.to_string(),
|
|
"title": model.title,
|
|
"description": model.description,
|
|
"position": model.position,
|
|
"assignee_id": model.assignee_id.map(|id| id.to_string()),
|
|
"priority": model.priority,
|
|
"created_at": model.created_at.to_rfc3339(),
|
|
"updated_at": model.updated_at.to_rfc3339(),
|
|
}))
|
|
}
|
|
|
|
// ─── update board card ────────────────────────────────────────────────────────
|
|
|
|
pub async fn update_board_card_exec(
|
|
ctx: ToolContext,
|
|
args: serde_json::Value,
|
|
) -> Result<serde_json::Value, ToolError> {
|
|
let project_id = ctx.project_id();
|
|
let sender_id = ctx
|
|
.sender_id()
|
|
.ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?;
|
|
let db = ctx.db();
|
|
|
|
require_admin(db, project_id, sender_id).await?;
|
|
|
|
let card_id = args
|
|
.get("card_id")
|
|
.and_then(|v| Uuid::parse_str(v.as_str()?).ok())
|
|
.ok_or_else(|| ToolError::ExecutionError("card_id is required".into()))?;
|
|
|
|
let card = ProjectBoardCard::find_by_id(card_id)
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
|
|
.ok_or_else(|| ToolError::ExecutionError("Card not found".into()))?;
|
|
|
|
// Verify card belongs to a column in this project's board
|
|
let col = ProjectBoardColumn::find_by_id(card.column)
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
|
|
.ok_or_else(|| ToolError::ExecutionError("Column not found".into()))?;
|
|
|
|
let board = ProjectBoard::find_by_id(col.board)
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
|
|
.ok_or_else(|| ToolError::ExecutionError("Board not found".into()))?;
|
|
|
|
if board.project != project_id {
|
|
return Err(ToolError::ExecutionError(
|
|
"Card does not belong to this project".into(),
|
|
));
|
|
}
|
|
|
|
let mut active: project_board_card::ActiveModel = card.clone().into();
|
|
let mut updated = false;
|
|
|
|
if let Some(title) = args.get("title").and_then(|v| v.as_str()) {
|
|
active.title = Set(title.to_string());
|
|
updated = true;
|
|
}
|
|
if let Some(description) = args.get("description") {
|
|
active.description = Set(description.as_str().map(|s| s.to_string()));
|
|
updated = true;
|
|
}
|
|
if let Some(column_id) = args
|
|
.get("column_id")
|
|
.and_then(|v| Uuid::parse_str(v.as_str()?).ok())
|
|
{
|
|
// Verify column belongs to the same board
|
|
let new_col = ProjectBoardColumn::find_by_id(column_id)
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
|
|
.ok_or_else(|| ToolError::ExecutionError("Column not found".into()))?;
|
|
if new_col.board != col.board {
|
|
return Err(ToolError::ExecutionError(
|
|
"Column does not belong to this board".into(),
|
|
));
|
|
}
|
|
active.column = Set(column_id);
|
|
updated = true;
|
|
}
|
|
if let Some(position) = args.get("position").and_then(|v| v.as_i64()) {
|
|
active.position = Set(position as i32);
|
|
updated = true;
|
|
}
|
|
if let Some(assignee_id) = args.get("assignee_id") {
|
|
active.assignee_id = Set(assignee_id.as_str().and_then(|s| Uuid::parse_str(s).ok()));
|
|
updated = true;
|
|
}
|
|
if let Some(priority) = args.get("priority") {
|
|
active.priority = Set(priority.as_str().map(|s| s.to_string()));
|
|
updated = true;
|
|
}
|
|
|
|
if !updated {
|
|
return Err(ToolError::ExecutionError(
|
|
"At least one field must be provided".into(),
|
|
));
|
|
}
|
|
|
|
active.updated_at = Set(Utc::now());
|
|
let model = active
|
|
.update(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
|
|
|
Ok(serde_json::json!({
|
|
"id": model.id.to_string(),
|
|
"column_id": model.column.to_string(),
|
|
"title": model.title,
|
|
"description": model.description,
|
|
"position": model.position,
|
|
"assignee_id": model.assignee_id.map(|id| id.to_string()),
|
|
"priority": model.priority,
|
|
"created_at": model.created_at.to_rfc3339(),
|
|
"updated_at": model.updated_at.to_rfc3339(),
|
|
}))
|
|
}
|
|
|
|
// ─── delete board card ─────────────────────────────────────────────────────────
|
|
|
|
pub async fn delete_board_card_exec(
|
|
ctx: ToolContext,
|
|
args: serde_json::Value,
|
|
) -> Result<serde_json::Value, ToolError> {
|
|
let project_id = ctx.project_id();
|
|
let sender_id = ctx
|
|
.sender_id()
|
|
.ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?;
|
|
let db = ctx.db();
|
|
|
|
require_admin(db, project_id, sender_id).await?;
|
|
|
|
let card_id = args
|
|
.get("card_id")
|
|
.and_then(|v| Uuid::parse_str(v.as_str()?).ok())
|
|
.ok_or_else(|| ToolError::ExecutionError("card_id is required".into()))?;
|
|
|
|
let card = ProjectBoardCard::find_by_id(card_id)
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
|
|
.ok_or_else(|| ToolError::ExecutionError("Card not found".into()))?;
|
|
|
|
let col = ProjectBoardColumn::find_by_id(card.column)
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
|
|
.ok_or_else(|| ToolError::ExecutionError("Column not found".into()))?;
|
|
|
|
let board = ProjectBoard::find_by_id(col.board)
|
|
.one(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
|
|
.ok_or_else(|| ToolError::ExecutionError("Board not found".into()))?;
|
|
|
|
if board.project != project_id {
|
|
return Err(ToolError::ExecutionError(
|
|
"Card does not belong to this project".into(),
|
|
));
|
|
}
|
|
|
|
ProjectBoardCard::delete_by_id(card_id)
|
|
.exec(db)
|
|
.await
|
|
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
|
|
|
Ok(serde_json::json!({ "deleted": true }))
|
|
}
|
|
|
|
// ─── tool definitions ─────────────────────────────────────────────────────────
|
|
|
|
pub fn list_tool_definition() -> ToolDefinition {
|
|
ToolDefinition::new("project_list_boards")
|
|
.description(
|
|
"List all Kanban boards in the current project. \
|
|
Returns boards with their columns and cards, including positions and priorities.",
|
|
)
|
|
.parameters(ToolSchema {
|
|
schema_type: "object".into(),
|
|
properties: None,
|
|
required: None,
|
|
})
|
|
}
|
|
|
|
pub fn create_board_tool_definition() -> ToolDefinition {
|
|
let mut p = HashMap::new();
|
|
p.insert("name".into(), ToolParam {
|
|
name: "name".into(), param_type: "string".into(),
|
|
description: Some("Board name (required).".into()),
|
|
required: true, properties: None, items: None,
|
|
});
|
|
p.insert("description".into(), ToolParam {
|
|
name: "description".into(), param_type: "string".into(),
|
|
description: Some("Board description. Optional.".into()),
|
|
required: false, properties: None, items: None,
|
|
});
|
|
ToolDefinition::new("project_create_board")
|
|
.description(
|
|
"Create a new Kanban board in the current project. Requires admin or owner role.",
|
|
)
|
|
.parameters(ToolSchema {
|
|
schema_type: "object".into(),
|
|
properties: Some(p),
|
|
required: Some(vec!["name".into()]),
|
|
})
|
|
}
|
|
|
|
pub fn update_board_tool_definition() -> ToolDefinition {
|
|
let mut p = HashMap::new();
|
|
p.insert("board_id".into(), ToolParam {
|
|
name: "board_id".into(), param_type: "string".into(),
|
|
description: Some("Board UUID (required).".into()),
|
|
required: true, properties: None, items: None,
|
|
});
|
|
p.insert("name".into(), ToolParam {
|
|
name: "name".into(), param_type: "string".into(),
|
|
description: Some("New board name. Optional.".into()),
|
|
required: false, properties: None, items: None,
|
|
});
|
|
p.insert("description".into(), ToolParam {
|
|
name: "description".into(), param_type: "string".into(),
|
|
description: Some("New board description. Optional.".into()),
|
|
required: false, properties: None, items: None,
|
|
});
|
|
ToolDefinition::new("project_update_board")
|
|
.description(
|
|
"Update a Kanban board (name or description). Requires admin or owner role.",
|
|
)
|
|
.parameters(ToolSchema {
|
|
schema_type: "object".into(),
|
|
properties: Some(p),
|
|
required: Some(vec!["board_id".into()]),
|
|
})
|
|
}
|
|
|
|
pub fn create_card_tool_definition() -> ToolDefinition {
|
|
let mut p = HashMap::new();
|
|
p.insert("board_id".into(), ToolParam {
|
|
name: "board_id".into(), param_type: "string".into(),
|
|
description: Some("Board UUID (required).".into()),
|
|
required: true, properties: None, items: None,
|
|
});
|
|
p.insert("column_id".into(), ToolParam {
|
|
name: "column_id".into(), param_type: "string".into(),
|
|
description: Some("Column UUID. Optional — defaults to first column.".into()),
|
|
required: false, properties: None, items: None,
|
|
});
|
|
p.insert("title".into(), ToolParam {
|
|
name: "title".into(), param_type: "string".into(),
|
|
description: Some("Card title (required).".into()),
|
|
required: true, properties: None, items: None,
|
|
});
|
|
p.insert("description".into(), ToolParam {
|
|
name: "description".into(), param_type: "string".into(),
|
|
description: Some("Card description. Optional.".into()),
|
|
required: false, properties: None, items: None,
|
|
});
|
|
p.insert("priority".into(), ToolParam {
|
|
name: "priority".into(), param_type: "string".into(),
|
|
description: Some("Card priority (e.g. 'low', 'medium', 'high'). Optional.".into()),
|
|
required: false, properties: None, items: None,
|
|
});
|
|
p.insert("assignee_id".into(), ToolParam {
|
|
name: "assignee_id".into(), param_type: "string".into(),
|
|
description: Some("Assignee user UUID. Optional.".into()),
|
|
required: false, properties: None, items: None,
|
|
});
|
|
ToolDefinition::new("project_create_board_card")
|
|
.description(
|
|
"Create a card on a Kanban board. If column_id is not provided, \
|
|
the card is added to the first column.",
|
|
)
|
|
.parameters(ToolSchema {
|
|
schema_type: "object".into(),
|
|
properties: Some(p),
|
|
required: Some(vec!["board_id".into(), "title".into()]),
|
|
})
|
|
}
|
|
|
|
pub fn update_card_tool_definition() -> ToolDefinition {
|
|
let mut p = HashMap::new();
|
|
p.insert("card_id".into(), ToolParam {
|
|
name: "card_id".into(), param_type: "string".into(),
|
|
description: Some("Card UUID (required).".into()),
|
|
required: true, properties: None, items: None,
|
|
});
|
|
p.insert("title".into(), ToolParam {
|
|
name: "title".into(), param_type: "string".into(),
|
|
description: Some("New card title. Optional.".into()),
|
|
required: false, properties: None, items: None,
|
|
});
|
|
p.insert("description".into(), ToolParam {
|
|
name: "description".into(), param_type: "string".into(),
|
|
description: Some("New card description. Optional.".into()),
|
|
required: false, properties: None, items: None,
|
|
});
|
|
p.insert("column_id".into(), ToolParam {
|
|
name: "column_id".into(), param_type: "string".into(),
|
|
description: Some("Move card to a different column. Optional.".into()),
|
|
required: false, properties: None, items: None,
|
|
});
|
|
p.insert("position".into(), ToolParam {
|
|
name: "position".into(), param_type: "integer".into(),
|
|
description: Some("New position within column. Optional.".into()),
|
|
required: false, properties: None, items: None,
|
|
});
|
|
p.insert("priority".into(), ToolParam {
|
|
name: "priority".into(), param_type: "string".into(),
|
|
description: Some("New priority. Optional.".into()),
|
|
required: false, properties: None, items: None,
|
|
});
|
|
p.insert("assignee_id".into(), ToolParam {
|
|
name: "assignee_id".into(), param_type: "string".into(),
|
|
description: Some("New assignee UUID. Optional.".into()),
|
|
required: false, properties: None, items: None,
|
|
});
|
|
ToolDefinition::new("project_update_board_card")
|
|
.description(
|
|
"Update a board card (title, description, column, position, assignee, priority). \
|
|
Requires admin or owner role.",
|
|
)
|
|
.parameters(ToolSchema {
|
|
schema_type: "object".into(),
|
|
properties: Some(p),
|
|
required: Some(vec!["card_id".into()]),
|
|
})
|
|
}
|
|
|
|
pub fn delete_card_tool_definition() -> ToolDefinition {
|
|
let mut p = HashMap::new();
|
|
p.insert("card_id".into(), ToolParam {
|
|
name: "card_id".into(), param_type: "string".into(),
|
|
description: Some("Card UUID (required).".into()),
|
|
required: true, properties: None, items: None,
|
|
});
|
|
ToolDefinition::new("project_delete_board_card")
|
|
.description("Delete a board card. Requires admin or owner role.")
|
|
.parameters(ToolSchema {
|
|
schema_type: "object".into(),
|
|
properties: Some(p),
|
|
required: Some(vec!["card_id".into()]),
|
|
})
|
|
}
|