UNPKG

agency-x

Version:

This project is a Claude-compatible, LLM-agnostic sub-agent framework that simulates a complete SaaS product team. It is delivered as a reusable, developer-ready NPM package.

340 lines (313 loc) 11.6 kB
#!/usr/bin/env node "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // src/utils/contextStore.ts var import_promises = __toESM(require("fs/promises")); var import_path = __toESM(require("path")); var sessionsDir = import_path.default.join(process.cwd(), "sessions"); var currentContext = {}; var generateSpecId = () => { const date = /* @__PURE__ */ new Date(); const year = date.getFullYear(); const month = (date.getMonth() + 1).toString().padStart(2, "0"); const day = date.getDate().toString().padStart(2, "0"); const randomId = Math.random().toString(36).substring(2, 6).toUpperCase(); return `SPEC-${year}${month}${day}-${randomId}`; }; var createContext = (featurePrompt) => { const specId = generateSpecId(); currentContext = { specId, featurePrompt, spec: {}, agents: {}, finalBundle: {} }; return currentContext; }; var getContext = () => currentContext; var updateContext = (updates) => { currentContext = { ...currentContext, ...updates }; return currentContext; }; var saveContext = async () => { const filePath = import_path.default.join(sessionsDir, `${currentContext.specId}.json`); await import_promises.default.writeFile(filePath, JSON.stringify(currentContext, null, 2)); return filePath; }; // src/utils/logger.ts var import_chalk = __toESM(require("chalk")); var agentStyles = { productManager: import_chalk.default.bold.blue, frontendDeveloper: import_chalk.default.green, backendDeveloper: import_chalk.default.yellow.italic, qaEngineer: import_chalk.default.red.underline, documentationAgent: import_chalk.default.cyan, fullstackIntegrator: import_chalk.default.magenta, infraEngineer: import_chalk.default.gray, securityEngineer: import_chalk.default.yellow, uxDesigner: import_chalk.default.blue, copywriter: import_chalk.default.green, releaseManager: import_chalk.default.magenta.bold, aiPromptEngineer: import_chalk.default.blue.italic, dataEngineer: import_chalk.default.yellow, orchestratorAgent: import_chalk.default.bold.magenta, voiceNarratorAgent: import_chalk.default.italic.gray, reviewerAgent: import_chalk.default.bold.yellow, assumptionChallenger: import_chalk.default.bold.red, userEmpathyAgent: import_chalk.default.bold.green }; var log = (agentName, message) => { const style = agentStyles[agentName] || ((str) => str); console.log(`${style(`[${agentName}]`)}: ${message}`); }; var spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"]; var interval; var startThinking = (agentName) => { let i = 0; const thinkingMessage = `is thinking...`; interval = setInterval(() => { i = (i + 1) % spinner.length; process.stdout.write(`\r${agentStyles[agentName](`[${agentName}]`)}: ${spinner[i]} ${thinkingMessage}`); }, 100); }; var stopThinking = () => { clearInterval(interval); process.stdout.write("\r" + " ".repeat(50) + "\r"); }; var createLogger = (agentName) => ({ log: (message) => log(agentName, message), start: () => startThinking(agentName), stop: () => stopThinking(), info: (message) => log(agentName, import_chalk.default.white(message)), success: (message) => log(agentName, import_chalk.default.greenBright(message)), error: (message) => log(agentName, import_chalk.default.redBright(message)) }); var orchestratorLogger = createLogger("orchestratorAgent"); // src/utils/voice.ts var import_say = require("say"); var voiceEnabled = false; var say = new import_say.Say(); var setVoiceEnabled = (enabled) => { voiceEnabled = enabled; }; var speak = (text) => { if (voiceEnabled) { say.speak(text); } }; var stop = () => { say.stop(); }; var narrator = { speak, stop }; // src/agents/productManager.ts var logger = createLogger("productManager"); var runProductManager = async () => { logger.start(); const context = getContext(); const { featurePrompt } = context; const spec = { title: `Spec for: ${featurePrompt}`, userStories: [ "As a user, I want to be able to export filtered reports as CSVs so that I can analyze the data in a spreadsheet." ], acceptanceCriteria: [ "The CSV export button should be disabled when no filters are applied.", "The exported CSV should contain all the data from the filtered report.", "The CSV should be well-formed and open correctly in Excel and Google Sheets." ], edgeCases: [ "What happens if the report contains no data?", "What happens if the user tries to export a very large report?" ] }; updateContext({ spec }); logger.stop(); logger.success("Generated product spec."); return spec; }; // src/agents/backendDeveloper.ts var logger2 = createLogger("backendDeveloper"); var runBackendDeveloper = async () => { logger2.start(); const context = getContext(); const { spec } = context; const code = ` const express = require('express'); const router = express.Router(); router.post('/export', (req, res) => { const { filters } = req.body; // In a real implementation, this would query the database with the filters. const data = [{ id: 1, name: 'Test' }]; res.csv(data, true); }); module.exports = router; `; updateContext({ agents: { ...context.agents, backendDeveloper: { output: code, completed: true } } }); logger2.stop(); logger2.success("Generated backend code."); return code; }; // src/agents/frontendDeveloper.ts var logger3 = createLogger("frontendDeveloper"); var runFrontendDeveloper = async () => { logger3.start(); const context = getContext(); const { spec } = context; const code = ` import React, { useState } from 'react'; const ExportButton = () => { const [loading, setLoading] = useState(false); const handleClick = async () => { setLoading(true); const response = await fetch('/api/export', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filters: {} }), }); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'report.csv'; a.click(); setLoading(false); }; return ( <button onClick={handleClick} disabled={loading}> {loading ? 'Exporting...' : 'Export as CSV'} </button> ); }; export default ExportButton; `; updateContext({ agents: { ...context.agents, frontendDeveloper: { output: code, completed: true } } }); logger3.stop(); logger3.success("Generated frontend code."); return code; }; // src/agents/fullstackIntegrator.ts var logger4 = createLogger("fullstackIntegrator"); var runFullstackIntegrator = async () => { logger4.start(); const context = getContext(); logger4.info("Ensuring FE and BE contract alignment."); updateContext({ agents: { ...context.agents, fullstackIntegrator: { completed: true } } }); logger4.stop(); logger4.success("Fullstack integration complete."); return true; }; // src/agents/qaEngineer.ts var logger5 = createLogger("qaEngineer"); var runQaEngineer = async () => { logger5.start(); const context = getContext(); const { spec, agents } = context; const tests = ` describe('CSV Export', () => { it('should export a CSV file when the export button is clicked', () => { // This is a mock test that would need to be implemented with a testing library like Jest and React Testing Library. expect(true).toBe(true); }); }); `; updateContext({ agents: { ...context.agents, qaEngineer: { tests, passed: true, completed: true } } }); logger5.stop(); logger5.success("Generated QA tests."); return tests; }; // src/agents/documentationAgent.ts var logger6 = createLogger("documentationAgent"); var runDocumentationAgent = async () => { logger6.start(); const context = getContext(); const { spec } = context; const doc = ` # CSV Export Feature This document describes the CSV export feature. ## User Stories - As a user, I want to be able to export filtered reports as CSVs so that I can analyze the data in a spreadsheet. ## Acceptance Criteria - The CSV export button should be disabled when no filters are applied. - The exported CSV should contain all the data from the filtered report. - The CSV should be well-formed and open correctly in Excel and Google Sheets. ## Edge Cases - What happens if the report contains no data? - What happens if the user tries to export a very large report? `; updateContext({ agents: { ...context.agents, documentationAgent: { doc, completed: true } } }); logger6.stop(); logger6.success("Generated documentation."); return doc; }; // src/orchestrator/orchestratorAgent.ts var runOrchestrator = async (options) => { const { feature: feature2, voice: voice2 = false } = options; setVoiceEnabled(voice2); orchestratorLogger.info(`Starting orchestration for feature: "${feature2}"`); narrator.speak(`Starting orchestration for feature: ${feature2}`); createContext(feature2); await runProductManager(); narrator.speak("Product manager has completed the spec."); await runBackendDeveloper(); narrator.speak("Backend developer has implemented the API."); await runFrontendDeveloper(); narrator.speak("Frontend developer has built the UI."); await runFullstackIntegrator(); narrator.speak("Fullstack integrator has verified the contracts."); await runQaEngineer(); narrator.speak("QA engineer has written the tests."); await runDocumentationAgent(); narrator.speak("Documentation agent has generated the guides."); const context = getContext(); const finalBundle = { code: { frontend: context.agents.frontendDeveloper.output, backend: context.agents.backendDeveloper.output }, tests: context.agents.qaEngineer.tests, docs: context.agents.documentationAgent.doc }; updateContext({ finalBundle }); const savedPath = await saveContext(); orchestratorLogger.success(`Orchestration complete. Context saved to ${savedPath}`); narrator.speak("All agents have completed their tasks. The final bundle is ready."); return getContext(); }; // src/cli/index.ts var args = process.argv.slice(2); var featureIndex = args.indexOf("--feature"); var feature = ""; if (featureIndex !== -1 && args[featureIndex + 1]) { feature = args[featureIndex + 1]; } if (!feature) { console.error("Please provide a feature description using the --feature flag."); process.exit(1); } var voice = args.includes("--voice"); runOrchestrator({ feature, voice });