Skip to main content
Glama
specify.ts10.7 kB
import { z } from 'zod'; import * as fs from 'fs/promises'; import * as path from 'path'; import { tmpdir } from 'os'; import { detectProjectType, checkSpecKitPrerequisites, findSpecsDirectory, createFeatureDirectory } from '../speckit/detector.js'; import { generateSpecFromTemplate } from '../speckit/templates.js'; import { resolveWorkspacePath } from './workspace.js'; /** * Spec Kit specify tools - Real Spec Kit Integration */ // Tool schemas export const SpecifyStartSchema = z.object({ projectName: z.string().describe('Name of the project to initialize'), agent: z.enum(['claude', 'copilot', 'gemini']).describe('AI agent to use'), workspacePath: z.string().optional().describe('Workspace directory path'), }); export const SpecifyDescribeSchema = z.object({ description: z.string().describe('Project specification description'), workspacePath: z.string().optional().describe('Workspace directory path'), }); /** * Initialize a new spec-driven project */ export async function specifyStart(params: z.infer<typeof SpecifyStartSchema>) { const { projectName, agent, workspacePath } = params; // Resolve workspace path with safe fallbacks const resolvedPath = resolveWorkspacePath(workspacePath); // Validate workspace path await validateWorkspacePath(resolvedPath); try { // Check project type and prerequisites const detection = await detectProjectType(resolvedPath); const prerequisites = await checkSpecKitPrerequisites(); // Determine specs directory const specsDir = await findSpecsDirectory(resolvedPath); // Create feature directory const featurePath = await createFeatureDirectory(specsDir, projectName); // Generate initial spec from template const specContent = await generateSpecFromTemplate( `Initialize ${projectName} project with ${agent} agent`, projectName ); // Write spec.md const specPath = path.join(featurePath, 'spec.md'); await fs.writeFile(specPath, specContent, 'utf-8'); // Create contracts directory const contractsPath = path.join(featurePath, 'contracts'); await fs.mkdir(contractsPath, { recursive: true }); // Create initial research.md const researchContent = `# Research Documentation ## Project: ${projectName} **Created:** ${new Date().toISOString()} **AI Agent:** ${agent} --- ## Technical Decisions _Document architectural decisions, trade-offs, and research findings here._ ## References _Add links to relevant documentation, articles, and resources._ `; const researchPath = path.join(featurePath, 'research.md'); await fs.writeFile(researchPath, researchContent, 'utf-8'); // Also maintain .dincoder compatibility for now const dincoderPath = path.join(resolvedPath, '.dincoder'); await fs.mkdir(dincoderPath, { recursive: true }); // Create compatibility JSON files in .dincoder const specJson = { projectName, agent, version: '1.0.0', createdAt: new Date().toISOString(), description: '', specKitPath: specPath, goals: [], requirements: { functional: [], nonFunctional: [], technical: [] } }; await fs.writeFile( path.join(dincoderPath, 'spec.json'), JSON.stringify(specJson, null, 2), 'utf-8' ); return { success: true, projectPath: featurePath, message: `Initialized Spec Kit project: ${projectName}`, details: { agent, projectType: detection.type, specKitAvailable: prerequisites.python && prerequisites.uv, filesCreated: { spec: specPath, research: researchPath, contracts: contractsPath, compatibility: path.join(dincoderPath, 'spec.json') }, nextSteps: [ 'Use specify_describe to add project details', 'Use plan_create to generate technical plan', 'Use tasks_generate to create actionable tasks' ] }, }; } catch (error) { throw new Error(`Failed to initialize project: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Create or update project specification */ export async function specifyDescribe(params: z.infer<typeof SpecifyDescribeSchema>) { const { description, workspacePath } = params; // Resolve workspace path with safe fallbacks const resolvedPath = resolveWorkspacePath(workspacePath); // Validate workspace path await validateWorkspacePath(resolvedPath); try { // Check project type const detection = await detectProjectType(resolvedPath); // Find the specs directory const specsDir = await findSpecsDirectory(resolvedPath); // Find the most recent feature directory let featurePath: string | null = null; let specPath: string | null = null; try { const entries = await fs.readdir(specsDir); const featureDirs = entries .filter(entry => /^\d{3}-/.test(entry)) .sort() .reverse(); if (featureDirs.length > 0) { featurePath = path.join(specsDir, featureDirs[0]); specPath = path.join(featurePath, 'spec.md'); // Check if spec.md exists const specExists = await fs.access(specPath).then(() => true).catch(() => false); if (!specExists) { specPath = null; } } } catch { // specs directory doesn't exist yet } if (!specPath) { // No existing spec, create a new feature const projectName = description.split(' ')[0] || 'unnamed-feature'; featurePath = await createFeatureDirectory(specsDir, projectName); // Generate spec from template with description const specContent = await generateSpecFromTemplate(description, projectName); specPath = path.join(featurePath, 'spec.md'); await fs.writeFile(specPath, specContent, 'utf-8'); } else { // Update existing spec with new description let specContent = await fs.readFile(specPath, 'utf-8'); // Update the user description in the spec const inputPattern = /\*\*Input\*\*: User description: "[^"]*"/; if (inputPattern.test(specContent)) { specContent = specContent.replace( inputPattern, `**Input**: User description: "${description}"` ); } else { // Add user description if not present const insertPoint = specContent.indexOf('## User Scenarios'); if (insertPoint > -1) { const before = specContent.substring(0, insertPoint); const after = specContent.substring(insertPoint); specContent = `${before}\n**Input**: User description: "${description}"\n\n${after}`; } } // Mark as updated const datePattern = /\*\*Created\*\*: [^\n]+/; const today = new Date().toISOString().split('T')[0]; if (datePattern.test(specContent)) { specContent = specContent.replace(datePattern, `**Created**: ${today} | **Updated**: ${new Date().toISOString()}`); } await fs.writeFile(specPath, specContent, 'utf-8'); } // Also update .dincoder compatibility files const dincoderPath = path.join(resolvedPath, '.dincoder'); const dincoderExists = await fs.access(dincoderPath).then(() => true).catch(() => false); if (dincoderExists) { const specJsonPath = path.join(dincoderPath, 'spec.json'); const specJsonExists = await fs.access(specJsonPath).then(() => true).catch(() => false); let specJson: any = {}; if (specJsonExists) { const content = await fs.readFile(specJsonPath, 'utf-8'); specJson = JSON.parse(content); } // Update with new description specJson.description = description; specJson.updatedAt = new Date().toISOString(); specJson.specKitPath = specPath; // Parse description for patterns const lines = description.split('\n').filter(line => line.trim()); // Extract goals const goals = lines.filter(line => line.match(/^(-\s*)?(goal|objective|aim)s?:?\s*/i) ).map(line => line.replace(/^(-\s*)?(goal|objective|aim)s?:?\s*/i, '').trim()); if (goals.length > 0) { specJson.goals = [...(specJson.goals || []), ...goals]; } // Extract requirements const requirements = lines.filter(line => line.match(/\b(must|should|require[ds]?|need[s]?)\b/i) ).map(line => line.trim()); if (requirements.length > 0) { specJson.requirements = specJson.requirements || {}; specJson.requirements.functional = [...(specJson.requirements.functional || []), ...requirements]; } await fs.writeFile(specJsonPath, JSON.stringify(specJson, null, 2), 'utf-8'); } return { success: true, specPath, message: 'Updated project specification', details: { location: featurePath, projectType: detection.type, updatedFields: { description: true, specKitFormat: true }, nextSteps: [ 'Review and edit spec.md directly for fine-tuning', 'Use plan_create to generate technical plan', 'Use artifacts_read to view current specification' ] }, }; } catch (error) { throw new Error(`Failed to update specification: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Validate workspace path for security */ async function validateWorkspacePath(workspacePath: string): Promise<void> { // Check if path exists try { const stat = await fs.stat(workspacePath); if (!stat.isDirectory()) { throw new Error('Workspace path is not a directory'); } } catch (error) { if ((error as any).code === 'ENOENT') { throw new Error('Workspace path does not exist'); } throw error; } // Prevent access to system directories (but allow temp dirs like /var/folders on macOS) const restrictedPaths = ['/etc', '/usr', '/bin', '/sbin']; const normalizedPath = path.normalize(workspacePath).toLowerCase(); // Check for exact matches or subdirectories (but not /var/folders or Node tmpdir) const tmpDir = tmpdir().toLowerCase(); for (const restricted of restrictedPaths) { if (normalizedPath === restricted || (normalizedPath.startsWith(restricted + '/') && !normalizedPath.startsWith(tmpDir))) { throw new Error(`Access to system directory ${restricted} is not allowed`); } } // Note: Removed git check as it's too restrictive for new projects }

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/flight505/MCP_DinCoder'

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