Skip to main content
Glama
backlog-write.ts10.6 kB
import { tool } from "@opencode-ai/plugin"; import { renameSync } from 'fs'; import { readFile, writeFile, access } from 'fs/promises'; import { parseBacklogFile, listBacklogItems, getNextVersion, handleListBacklog, validateStatusTransition, generateBacklogFilename, createBacklogTemplate, amendBacklogTemplate, readBacklogFile, serializeFrontmatter, updateBacklogFrontmatter } from '../lib/backlog-shared'; import { getBacklogDir, getCompletedBacklogDir, resolveBacklogPath } from '../lib/path-resolver'; /** * Check if a file exists using fs/promises * @param path Path to the file * @returns True if file exists */ async function fileExists(path: string): Promise<boolean> { try { await access(path); return true; } catch { return false; } } async function handleCreate(args, context) { const { topic, description, priority = "medium" } = args; if (!topic || !description) { throw new Error("topic and description are required for create action"); } const filename = generateBacklogFilename(topic); const backlogDir = getBacklogDir(); const dirpath = resolveBacklogPath('Backlog', filename); const filepath = resolveBacklogPath('Backlog', filename, 'item.md'); // Check for duplicate (both new and legacy structure) const newExists = await fileExists(filepath); const legacyPath = resolveBacklogPath('Backlog', `${filename}.md`); const legacyExists = await fileExists(legacyPath); if (newExists || legacyExists) { throw new Error(`Backlog item already exists. Use 'amend' to update it.`); } const content = createBacklogTemplate(topic, description, priority, context); // Create directory structure await writeFile(filepath, content); return `Created backlog item: ${filepath}\nNext: Use backlog-todo-write to add todos, then submit when ready`; } async function handleAmend(args, context) { const { topic, description, status, priority } = args; if (!topic) { throw new Error("topic is required for amend action"); } const filename = generateBacklogFilename(topic); const backlogDir = getBacklogDir(); const dirpath = resolveBacklogPath('Backlog', filename); const filepath = resolveBacklogPath('Backlog', filename, 'item.md'); const legacyPath = resolveBacklogPath('Backlog', `${filename}.md`); // Check both new and legacy paths let actualPath = filepath; const newExists = await fileExists(filepath); const legacyExists = await fileExists(legacyPath); if (!newExists && !legacyExists) { throw new Error(`Backlog item not found: ${filepath}`); } if (legacyExists && !newExists) { actualPath = legacyPath; } // Parse current version const currentData = await parseBacklogFile(actualPath); // Use provided values or keep current values const newStatus = status || currentData.status; const newPriority = priority || currentData.priority; // Validate status transition if status is being changed if (status) { validateStatusTransition(currentData.status, newStatus); } const nextVersion = getNextVersion(filename); // Move current version to archive const completedDir = getCompletedBacklogDir(); const archivePath = resolveBacklogPath('COMPLETED_Backlog', `${filename}-v${nextVersion}.md`); renameSync(actualPath, archivePath); // Create new version (always use new structure) const newContent = amendBacklogTemplate( topic, description || '(No updated description provided)', newPriority, newStatus, nextVersion + 1, currentData.created, currentData.agent || 'unknown', currentData.session || 'unknown', context ); await writeFile(filepath, newContent); const updates = []; if (status) updates.push(`status=${status}`); if (priority) updates.push(`priority=${priority}`); if (description) updates.push('description'); const updateInfo = updates.length > 0 ? ` (updated: ${updates.join(', ')})` : ''; return `Amended backlog item: ${filepath}${updateInfo} (archived v${nextVersion} to ${archivePath})`; } async function handleSubmit(args, context) { const { topic } = args; if (!topic) { throw new Error("topic is required for submit action"); } const filename = generateBacklogFilename(topic); const filepath = resolveBacklogPath('Backlog', filename, 'item.md'); const legacyPath = resolveBacklogPath('Backlog', `${filename}.md`); // Check both paths const newExists = await fileExists(filepath); const legacyExists = await fileExists(legacyPath); const actualPath = newExists ? filepath : (legacyExists ? legacyPath : null); if (!actualPath) { throw new Error(`Backlog item not found`); } // Parse current version const currentData = await parseBacklogFile(actualPath); // Validate current status is 'new' if (currentData.status !== 'new') { throw new Error(`Cannot submit item with status '${currentData.status}'. Item must be in 'new' status to submit.`); } // Use amend logic to transition to 'ready' const result = await handleAmend({ topic, status: 'ready' }, context); return result + '\nStatus: ready. Next: Create todos and begin work, then move to review'; } async function handleApprove(args, context) { const { topic } = args; if (!topic) { throw new Error("topic is required for approve action"); } const filename = generateBacklogFilename(topic); const filepath = resolveBacklogPath('Backlog', filename, 'item.md'); const legacyPath = resolveBacklogPath('Backlog', `${filename}.md`); // Check both paths const newExists = await fileExists(filepath); const legacyExists = await fileExists(legacyPath); const actualPath = newExists ? filepath : (legacyExists ? legacyPath : null); if (!actualPath) { throw new Error(`Backlog item not found`); } // Parse current version const currentData = await parseBacklogFile(actualPath); // Validate current status is 'review' if (currentData.status !== 'review') { throw new Error(`Cannot approve item with status '${currentData.status}'. Item must be in 'review' status to approve.`); } // Use amend logic to transition to 'done' const result = await handleAmend({ topic, status: 'done' }, context); return result + '\nStatus: done. Item completed successfully'; } async function handleReopen(args, context) { const { topic, description } = args; if (!topic) { throw new Error("topic is required for reopen action"); } const filename = generateBacklogFilename(topic); const filepath = resolveBacklogPath('Backlog', filename, 'item.md'); const legacyPath = resolveBacklogPath('Backlog', `${filename}.md`); // Check both paths const newExists = await fileExists(filepath); const legacyExists = await fileExists(legacyPath); const actualPath = newExists ? filepath : (legacyExists ? legacyPath : null); if (!actualPath) { throw new Error(`Backlog item not found`); } // Parse current version const currentData = await parseBacklogFile(actualPath); // Validate current status is 'review' or 'done' if (currentData.status !== 'review' && currentData.status !== 'done') { throw new Error(`Cannot reopen item with status '${currentData.status}'. Item must be in 'review' or 'done' status to reopen.`); } if (!description) { throw new Error("description (review notes) is required for reopen action"); } // Use amend logic to transition to 'reopen' with review notes const result = await handleAmend({ topic, status: 'reopen', description }, context); return result + '\nStatus: reopen. Next: Address review feedback and resubmit'; } async function handleWontfix(args, context) { const { topic, description } = args; if (!topic) { throw new Error("topic is required for wontfix action"); } const filename = generateBacklogFilename(topic); const filepath = resolveBacklogPath('Backlog', filename, 'item.md'); const legacyPath = resolveBacklogPath('Backlog', `${filename}.md`); // Check both paths const newExists = await fileExists(filepath); const legacyExists = await fileExists(legacyPath); const actualPath = newExists ? filepath : (legacyExists ? legacyPath : null); if (!actualPath) { throw new Error(`Backlog item not found`); } // Parse current version const currentData = await parseBacklogFile(actualPath); // Allow wontfix from any non-terminal state if (currentData.status === 'done' || currentData.status === 'wontfix') { throw new Error(`Cannot mark item with status '${currentData.status}' as wontfix. Item is already in a terminal state.`); } // Use amend logic to transition to 'wontfix' with optional reason const result = await handleAmend({ topic, status: 'wontfix', description }, context); return result + '\nStatus: wontfix. Item closed without completion'; } export default tool({ description: "Write access to backlog management - create, amend, and list backlog work items", args: { action: tool.schema .enum(["create", "list", "amend", "approve", "submit", "reopen", "wontfix"]) .optional() .describe("Operation to perform (default: create)"), topic: tool.schema .string() .optional() .describe("Topic name (required for create/amend)"), description: tool.schema .string() .optional() .describe("Description (required for create, optional for amend)"), priority: tool.schema .enum(["high", "medium", "low"]) .optional() .describe("Priority level for create/amend operations (default: medium)"), status: tool.schema .enum(["new", "ready", "review", "done", "reopen", "wontfix"]) .optional() .describe("Status for amend operation or filter for list operation"), }, async execute(args, context) { const action = args.action || "create"; switch (action) { case "create": return await handleCreate(args, context); case "list": return await handleListBacklog(args); case "amend": return await handleAmend(args, context); case "submit": return await handleSubmit(args, context); case "approve": return await handleApprove(args, context); case "reopen": return await handleReopen(args, context); case "wontfix": return await handleWontfix(args, context); default: throw new Error(`Unknown action: ${action}`); } } });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rwese/mcp-backlog'

If you have feedback or need assistance with the MCP directory API, please join our Discord server