const BASE = () => process.env.KANBOARD_URL || 'http://localhost:4800'; const BOT_USER = () => process.env.KANBOARD_BOT_USER || 'claude'; let _token = null; let _botId = null; export function initKanboard(token) { _token = token; console.log('Kanboard: ready'); } function token() { if (!_token) throw new Error('Kanboard not initialized'); return _token; } // JSON-RPC over the app-wide token (login "jsonrpc:") — full access to all projects. async function rpc(method, params = {}) { const auth = Buffer.from(`jsonrpc:${token()}`).toString('base64'); const res = await fetch(`${BASE()}/jsonrpc.php`, { method: 'POST', headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method, id: 1, params }), }); if (!res.ok) throw new Error(`Kanboard ${method}: ${res.status} ${await res.text()}`); const json = await res.json(); if (json.error) throw new Error(`Kanboard ${method}: ${json.error.message || JSON.stringify(json.error)}`); return json.result; } // Resolve + cache the bot user's id (default author for writes, filter for "my tasks"). async function botUserId() { if (_botId != null) return _botId; const user = await rpc('getUserByName', { username: BOT_USER() }); if (!user || !user.id) throw new Error(`Bot user not found in Kanboard: ${BOT_USER()}`); _botId = parseInt(user.id); return _botId; } const STATUS = { open: 1, closed: 0 }; // --- Read / monitor --- export async function kbListProjects() { const projects = await rpc('getAllProjects'); return projects.map(p => ({ id: parseInt(p.id), name: p.name, is_active: parseInt(p.is_active), is_private: parseInt(p.is_private), owner_id: parseInt(p.owner_id), })); } export async function kbGetProject(projectId) { const [project, columns, swimlanes] = await Promise.all([ rpc('getProjectById', { project_id: projectId }), rpc('getColumns', { project_id: projectId }), rpc('getAllSwimlanes', { project_id: projectId }), ]); if (!project) throw new Error(`Project not found: ${projectId}`); return { id: parseInt(project.id), name: project.name, description: project.description, is_active: parseInt(project.is_active), owner_id: parseInt(project.owner_id), columns: columns.map(c => ({ id: parseInt(c.id), title: c.title, position: parseInt(c.position) })), swimlanes: swimlanes.map(s => ({ id: parseInt(s.id), name: s.name, is_active: parseInt(s.is_active) })), }; } function compactTask(t) { return { id: parseInt(t.id), title: t.title, project_id: parseInt(t.project_id), column_id: parseInt(t.column_id), swimlane_id: parseInt(t.swimlane_id), owner_id: parseInt(t.owner_id), is_active: parseInt(t.is_active), color_id: t.color_id, priority: parseInt(t.priority || 0), date_due: parseInt(t.date_due) || 0, reference: t.reference || '', }; } export async function kbListTasks(projectId, status = 'open') { let tasks = []; if (status === 'all') { const [open, closed] = await Promise.all([ rpc('getAllTasks', { project_id: projectId, status_id: STATUS.open }), rpc('getAllTasks', { project_id: projectId, status_id: STATUS.closed }), ]); tasks = [...open, ...closed]; } else { tasks = await rpc('getAllTasks', { project_id: projectId, status_id: STATUS[status] ?? STATUS.open }); } return tasks.map(compactTask); } // Tasks assigned to the bot across every project. status: open | closed | all export async function kbMyTasks(status = 'open') { const botId = await botUserId(); const projects = await rpc('getAllProjects'); const out = []; for (const p of projects) { const tasks = await kbListTasks(parseInt(p.id), status); for (const t of tasks) { if (t.owner_id === botId) out.push({ ...t, project_name: p.name }); } } return out; } export async function kbGetTask(taskId) { const task = await rpc('getTask', { task_id: taskId }); if (!task) throw new Error(`Task not found: ${taskId}`); const [subtasks, comments] = await Promise.all([ rpc('getAllSubtasks', { task_id: taskId }), rpc('getAllComments', { task_id: taskId }), ]); return { task, subtasks: (subtasks || []).map(s => ({ id: parseInt(s.id), title: s.title, status: parseInt(s.status), status_name: s.status_name, user_id: parseInt(s.user_id) || 0, assignee: s.name || s.username || null, })), comments: (comments || []).map(c => ({ id: parseInt(c.id), user_id: parseInt(c.user_id), author: c.name || c.username, date: parseInt(c.date_creation), content: c.comment, })), }; } export async function kbSearchTasks(projectId, query) { const tasks = await rpc('searchTasks', { project_id: projectId, query }); return (tasks || []).map(compactTask); } export async function kbListUsers() { const users = await rpc('getAllUsers'); return users.map(u => ({ id: parseInt(u.id), username: u.username, name: u.name, role: u.role, is_active: parseInt(u.is_active), email: u.email, })); } export async function kbProjectActivity(projectId) { return rpc('getProjectActivity', { project_id: projectId }); } // --- Write (creation/comments authored by the bot user) --- export async function kbCreateTask(params) { const body = { ...params }; if (body.creator_id == null) body.creator_id = await botUserId(); const id = await rpc('createTask', body); if (!id) throw new Error('createTask returned false (check required fields: title, project_id)'); return { task_id: parseInt(id) }; } export async function kbUpdateTask(params) { const ok = await rpc('updateTask', params); return { updated: !!ok }; } // Move a card. project_id and swimlane_id are resolved from the task when omitted. export async function kbMoveTask({ task_id, column_id, position = 1, swimlane_id, project_id }) { if (project_id == null || swimlane_id == null) { const task = await rpc('getTask', { task_id }); if (!task) throw new Error(`Task not found: ${task_id}`); if (project_id == null) project_id = parseInt(task.project_id); if (swimlane_id == null) swimlane_id = parseInt(task.swimlane_id); } const ok = await rpc('moveTaskPosition', { project_id, task_id, column_id, position, swimlane_id }); return { moved: !!ok }; } export async function kbChangeTaskStatus(taskId, action) { const method = action === 'close' ? 'closeTask' : 'openTask'; const ok = await rpc(method, { task_id: taskId }); return { [action === 'close' ? 'closed' : 'opened']: !!ok }; } // Assign a task to a user (username or numeric id). export async function kbAssignTask(taskId, owner) { let ownerId = owner; if (typeof owner === 'string' && !/^\d+$/.test(owner)) { const user = await rpc('getUserByName', { username: owner }); if (!user || !user.id) throw new Error(`User not found: ${owner}`); ownerId = parseInt(user.id); } const ok = await rpc('updateTask', { id: taskId, owner_id: parseInt(ownerId) }); return { assigned: !!ok, owner_id: parseInt(ownerId) }; } export async function kbAddComment(taskId, content, userId) { const uid = userId != null ? userId : await botUserId(); const id = await rpc('createComment', { task_id: taskId, user_id: uid, content }); if (!id) throw new Error('createComment returned false'); return { comment_id: parseInt(id) }; } export async function kbCreateSubtask(params) { const body = { ...params }; if (body.user_id == null) body.user_id = await botUserId(); const id = await rpc('createSubtask', body); if (!id) throw new Error('createSubtask returned false (required: task_id, title)'); return { subtask_id: parseInt(id) }; } export async function kbUpdateSubtask(params) { const ok = await rpc('updateSubtask', params); return { updated: !!ok }; } export async function kbRemoveTask(taskId) { const ok = await rpc('removeTask', { task_id: taskId }); return { removed: !!ok }; } export async function kbRemoveComment(commentId) { const ok = await rpc('removeComment', { comment_id: commentId }); return { removed: !!ok }; }