Files
AgapHost/agap-mcp/src/kanboard.js
Alvis b8efe4732d Sync infra config: HA/Zabbix relocation, Immich storage move, new services
Accumulated uncommitted infra changes:
- Caddyfile: repoint HA/Zabbix to 192.168.1.4/.3, add ~20 new site routes
- Immich: move media to /mnt/smsg, enable CUDA ML, mem limits, rewrite backup.sh
- Add service stacks: agap-mcp, anki, family, freshrss, iperf3, kanboard,
  linkwarden, qbittorrent, radicale, syncthing, vikunja, windows
- openwebui: enable API keys; ollama: drop CPU fallback
- seafile/zabbix: extra_hosts entries; matrix: add user juris
- Remove pihole stack and stale wiki/migrate.py
- Ignore marketplace-mcp (standalone repo) and linkwarden runtime data

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
2026-07-04 13:28:04 +00:00

216 lines
7.9 KiB
JavaScript

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:<token>") — 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 };
}