import { existsSync } from "node:fs"; import { join } from "node:path"; import { regenerateAgentFiles } from "../lib/agent-files.ts"; import { AGENT_REGISTRY, addAgent, disableAgent, enableAgent, getActiveAgents, getAgentDefinition, getPreferredAgent, pathExists, removeAgent, scanAgents, setPreferredAgent, updateAgent, } from "../lib/agents.ts"; import { readConfig } from "../lib/config.ts"; import { error, info, item, output as outputSpinner, success, warn } from "../lib/output.ts"; import { readTemplateMetadata } from "../lib/project-link.ts"; import { getProjectNameFromDir } from "../lib/storage/index.ts"; import { resolveTemplate } from "../templates/index.ts"; import type { Template } from "../templates/types.ts"; /** * Main agents command - handles all agent management */ interface AgentsOptions { project?: string; } export default async function agents( subcommand?: string, args: string[] = [], options: AgentsOptions = {}, ): Promise { if (!subcommand) { return await listAgents(); } switch (subcommand) { case "scan": return await scanAndPrompt(); case "add": return await addAgentCommand(args); case "remove": return await removeAgentCommand(args); case "enable": return await enableAgentCommand(args); case "disable": return await disableAgentCommand(args); case "prefer": return await preferAgentCommand(args); case "refresh": return await refreshAgentFilesCommand(options); default: error(`Unknown subcommand: ${subcommand}`); info("Available: scan, add, remove, enable, disable, prefer, refresh"); process.exit(1); } } /** * List all agents with their status */ async function listAgents(): Promise { const config = await readConfig(); const configuredAgents = config?.agents || {}; const preferredAgentId = await getPreferredAgent(); console.error(""); info("AI Coding Agents"); console.error(""); for (const definition of AGENT_REGISTRY) { const agentConfig = configuredAgents[definition.id]; const isPreferred = definition.id === preferredAgentId; if (agentConfig) { const statusMark = agentConfig.active ? "✓" : "○"; const status = agentConfig.active ? "active" : "inactive"; const preferredLabel = isPreferred ? " ★ preferred" : ""; console.error(`${statusMark} ${definition.name} (${status})${preferredLabel}`); item(`Path: ${agentConfig.path}`); // Validate path still exists if (!pathExists(agentConfig.path)) { console.error(""); item("⚠ Path no longer exists - run: jack agents scan"); } } else { console.error(`○ ${definition.name} (not detected)`); } console.error(""); } info("Commands: jack agents scan | add | remove | enable | disable | prefer | refresh"); } /** * Scan for agents and prompt to enable new ones */ async function scanAndPrompt(): Promise { outputSpinner.start("Scanning for agents..."); const detectionResult = await scanAgents(); outputSpinner.stop(); if (detectionResult.detected.length === 0) { info("No agents detected"); await listAgents(); return; } const config = await readConfig(); const existingAgents = config?.agents || {}; const newAgents = detectionResult.detected.filter(({ id }) => !existingAgents[id]); if (newAgents.length === 0) { success("All agents up to date"); await listAgents(); return; } console.error(""); success(`Found ${newAgents.length} new agent(s):`); for (const { id, path, launch } of newAgents) { const definition = getAgentDefinition(id); item(`${definition?.name}: ${path}`); // Auto-enable (following omakase principle) await updateAgent(id, { active: true, path, detectedAt: new Date().toISOString(), launch, }); } console.error(""); success("New agents enabled"); info("Future projects will include context files for these agents"); await listAgents(); } function getFlagValues(args: string[], flag: string): string[] { const values: string[] = []; for (let i = 0; i < args.length; i++) { const current = args[i]; const next = args[i + 1]; if (current === flag && next) { values.push(next); i++; } } return values; } /** * Manually add an agent */ async function addAgentCommand(args: string[]): Promise { const [agentId, ...rest] = args; if (!agentId) { error("Agent ID required"); info("Usage: jack agents add [--command cmd] [--arg value]"); info(`Available IDs: ${AGENT_REGISTRY.map((a) => a.id).join(", ")}`); process.exit(1); } const definition = getAgentDefinition(agentId); if (!definition) { error(`Unknown agent: ${agentId}`); info(`Available: ${AGENT_REGISTRY.map((a) => a.id).join(", ")}`); process.exit(1); } // Parse flags let customCommand: string | undefined; for (let i = 0; i < rest.length; i++) { if (rest[i] === "--command" && rest[i + 1]) { customCommand = rest[i + 1]; break; } } const customArgs = getFlagValues(rest, "--arg"); if (customArgs.length > 0 && !customCommand) { error("Use --command with --arg"); info("Usage: jack agents add [--command cmd] [--arg value]"); process.exit(1); } try { const launchOverride = customCommand ? { type: "cli" as const, command: customCommand, args: customArgs.length ? customArgs : undefined, } : undefined; await addAgent(agentId, { launch: launchOverride }); success(`Added ${definition.name}`); const config = await readConfig(); const agentConfig = config?.agents?.[agentId]; if (agentConfig) { item(`Path: ${agentConfig.path}`); } info("Future projects will include context files for this agent"); } catch (err) { if (err instanceof Error) { error(err.message); if (err.message.includes("Could not detect")) { info(`Specify launch manually: jack agents add ${agentId} --command /path/to/command`); } } process.exit(1); } } /** * Remove an agent from config */ async function removeAgentCommand(args: string[]): Promise { const [agentId] = args; if (!agentId) { error("Agent ID required"); info("Usage: jack agents remove "); process.exit(1); } try { await removeAgent(agentId); success(`Removed ${agentId}`); } catch (err) { if (err instanceof Error) { error(err.message); } process.exit(1); } } /** * Enable an agent */ async function enableAgentCommand(args: string[]): Promise { const [agentId] = args; if (!agentId) { error("Agent ID required"); info("Usage: jack agents enable "); process.exit(1); } try { await enableAgent(agentId); success(`Enabled ${agentId}`); } catch (err) { if (err instanceof Error) { error(err.message); if (err.message.includes("not configured")) { info(`Run: jack agents add ${agentId}`); } } process.exit(1); } } /** * Disable an agent */ async function disableAgentCommand(args: string[]): Promise { const [agentId] = args; if (!agentId) { error("Agent ID required"); info("Usage: jack agents disable "); process.exit(1); } try { await disableAgent(agentId); success(`Disabled ${agentId}`); info("Future projects will not include context files for this agent"); } catch (err) { if (err instanceof Error) { error(err.message); } process.exit(1); } } /** * Set preferred agent */ async function preferAgentCommand(args: string[]): Promise { const [agentId] = args; if (!agentId) { // Show current preferred agent const preferred = await getPreferredAgent(); if (preferred) { const definition = getAgentDefinition(preferred); success(`Preferred agent: ${definition?.name || preferred}`); } else { info("No preferred agent set"); } info("Usage: jack agents prefer "); info(`Available: ${AGENT_REGISTRY.map((a) => a.id).join(", ")}`); return; } try { await setPreferredAgent(agentId); const definition = getAgentDefinition(agentId); success(`Set ${definition?.name || agentId} as preferred agent`); } catch (err) { if (err instanceof Error) { error(err.message); } process.exit(1); } } /** * Refresh agent context files from template */ async function refreshAgentFilesCommand(options: AgentsOptions = {}): Promise { const projectDir = process.cwd(); let projectName: string; if (options.project) { // When --project is specified, verify we're in that project's directory projectName = options.project; // Verify the current directory matches the project try { const cwdProjectName = await getProjectNameFromDir(projectDir); if (cwdProjectName !== projectName) { error(`Current directory is not the project "${projectName}"`); info(`Run this command from the ${projectName} project directory`); process.exit(1); } } catch { error("Current directory is not a valid project"); info(`Run this command from the ${projectName} project directory`); process.exit(1); } } else { // 1. Detect project name from wrangler config outputSpinner.start("Detecting project..."); try { projectName = await getProjectNameFromDir(projectDir); } catch { outputSpinner.stop(); error("Could not determine project"); info("Run this command from a project directory, or use --project "); process.exit(1); } outputSpinner.stop(); } // 2. Read template metadata from .jack/template.json const templateMetadata = await readTemplateMetadata(projectDir); if (!templateMetadata) { error("No template lineage found for this project"); info("This project was created before lineage tracking was added."); info("Re-create the project with `jack new` to enable refresh."); process.exit(1); } // 3. Resolve template (fetch if GitHub, load if builtin) outputSpinner.start("Loading template..."); let template: Template; try { template = await resolveTemplate(templateMetadata.name); } catch (err) { outputSpinner.stop(); error(`Failed to load template: ${templateMetadata.name}`); if (err instanceof Error) { info(err.message); } process.exit(1); } outputSpinner.stop(); // 4. Backup existing files const agentsMdPath = join(projectDir, "AGENTS.md"); if (existsSync(agentsMdPath)) { const backupPath = `${agentsMdPath}.backup`; const content = await Bun.file(agentsMdPath).text(); await Bun.write(backupPath, content); success("Backed up AGENTS.md → AGENTS.md.backup"); } const claudeMdPath = join(projectDir, "CLAUDE.md"); if (existsSync(claudeMdPath)) { const backupPath = `${claudeMdPath}.backup`; const content = await Bun.file(claudeMdPath).text(); await Bun.write(backupPath, content); success("Backed up CLAUDE.md → CLAUDE.md.backup"); } // 5. Get active agents const activeAgents = await getActiveAgents(); if (activeAgents.length === 0) { warn("No active agents configured"); info("Run: jack agents scan"); process.exit(1); } // 6. Regenerate agent files const updatedFiles = await regenerateAgentFiles(projectDir, projectName, template, activeAgents); console.error(""); success("Refreshed agent context files:"); for (const file of updatedFiles) { item(file); } console.error(""); info("Review changes: git diff AGENTS.md"); }