/** * Confluence Cloud REST API v2 client. * Uses basic auth with Atlassian API token. */ const BASE_URL = process.env.CONFLUENCE_URL || 'https://agiliton.atlassian.net/wiki'; const API_V2 = `${BASE_URL}/api/v2`; const API_V1 = `${BASE_URL}/rest/api`; function getAuth() { const user = process.env.CONFLUENCE_USER || process.env.JIRA_USER; const token = process.env.CONFLUENCE_API_TOKEN || process.env.JIRA_API_TOKEN; if (!user || !token) { throw new Error('CONFLUENCE_USER and CONFLUENCE_API_TOKEN (or JIRA_USER/JIRA_API_TOKEN) must be set'); } return Buffer.from(`${user}:${token}`).toString('base64'); } async function request(url, options = {}) { const headers = { 'Authorization': `Basic ${getAuth()}`, 'Accept': 'application/json', ...(options.headers || {}), }; if (options.body && typeof options.body === 'string') { headers['Content-Type'] = 'application/json'; } const response = await fetch(url, { ...options, headers }); if (!response.ok) { const text = await response.text().catch(() => ''); throw new Error(`Confluence API ${response.status}: ${text.slice(0, 500)}`); } if (response.status === 204) return null; return response.json(); } // --- Spaces --- export async function listSpaces(limit = 25) { const data = await request(`${API_V2}/spaces?limit=${limit}&sort=name`); return data.results; } export async function getSpace(spaceIdOrKey) { return request(`${API_V2}/spaces/${spaceIdOrKey}`); } export async function createSpace(key, name, description) { // v2 doesn't have space creation — use v1 const body = { key, name, type: 'global', }; if (description) { body.description = { plain: { value: description, representation: 'plain' }, }; } return request(`${API_V1}/space`, { method: 'POST', body: JSON.stringify(body), }); } // --- Pages --- export async function searchPages(query, spaceId, limit = 10) { let cql = `type=page AND text ~ "${query.replace(/"/g, '\\"')}"`; if (spaceId) cql += ` AND space.id=${spaceId}`; cql += ' ORDER BY lastmodified DESC'; const params = new URLSearchParams({ cql, limit: String(limit), }); // CQL search is v1 only const data = await request(`${API_V1}/content/search?${params}`); return data.results.map((r) => ({ id: r.id, status: r.status, title: r.title, spaceId: r.space?.id || '', authorId: r.history?.createdBy?.accountId || '', createdAt: r.history?.createdDate || '', version: { number: r.version?.number || 1, createdAt: r.version?.when || '', }, _links: { webui: r._links?.webui ? `${BASE_URL}${r._links.webui}` : undefined, }, })); } export async function getPage(pageId, includeBody = true) { const params = includeBody ? '?body-format=storage' : ''; return request(`${API_V2}/pages/${pageId}${params}`); } export async function getPageByTitle(spaceId, title) { const params = new URLSearchParams({ title, 'space-id': spaceId, 'body-format': 'storage', limit: '1', }); const data = await request(`${API_V2}/pages?${params}`); return data.results?.[0] || null; } export async function createPage(spaceId, title, body, parentId) { const payload = { spaceId, status: 'current', title, body: { representation: 'storage', value: body, }, }; if (parentId) payload.parentId = parentId; return request(`${API_V2}/pages`, { method: 'POST', body: JSON.stringify(payload), }); } export async function updatePage(pageId, title, body, versionNumber, versionMessage) { const payload = { id: pageId, status: 'current', title, body: { representation: 'storage', value: body, }, version: { number: versionNumber, message: versionMessage || 'Updated via Claude', }, }; return request(`${API_V2}/pages/${pageId}`, { method: 'PUT', body: JSON.stringify(payload), }); } // --- Comments --- export async function getPageComments(pageId, limit = 25) { const data = await request(`${API_V2}/pages/${pageId}/footer-comments?body-format=storage&limit=${limit}`); return data.results; } export async function addPageComment(pageId, body) { return request(`${API_V2}/footer-comments`, { method: 'POST', body: JSON.stringify({ pageId, body: { representation: 'storage', value: body, }, }), }); } // --- Utility --- export function getWebUrl(page) { if (page._links?.webui) { return page._links.webui.startsWith('http') ? page._links.webui : `${BASE_URL}${page._links.webui}`; } return `${BASE_URL}/pages/${page.id}`; }