import { homedir } from "node:os"; import { join } from "node:path"; import { parseArgs } from "node:util"; import { ModeRegistry, activateExtensions, createSession, discoverUserExtensions, findSkill, parseSkillInvocation, runTurn, type AgentSession, } from "@magiccode/core"; import { claudeCodeExtension } from "@magiccode/ext-claudecode"; import { createProvider, loadModelChoices } from "@magiccode/providers"; import { ClackUI } from "@magiccode/ui"; import { resolveConfig } from "./config"; import { resolveLaunchDir } from "./launch-dir"; const HELP = `MagicCode — assistant IA en ligne de commande Usage : magiccode [options] --mode Mode de fonctionnement (défaut : claudecode) --model Modèle de la session, ex. sapaicore:anthropic--claude-4.5-sonnet Sans cette option, la liste des modèles disponibles est proposée au démarrage. -h, --help Affiche cette aide Commandes en session : /help /mode /model /clear /exit Skills de l'agent : §nom_du_skill [arguments] (§ seul : liste des skills)`; async function main(): Promise { const { values } = parseArgs({ options: { mode: { type: "string" }, model: { type: "string" }, help: { type: "boolean", short: "h" }, }, }); if (values.help) { console.log(HELP); return; } // Dossier de lancement (voir launch-dir.ts) : base de tous les chemins relatifs const cwd = resolveLaunchDir(); try { process.chdir(cwd); } catch (err) { console.error(`Dossier de lancement inaccessible : ${cwd} (${(err as Error).message})`); process.exitCode = 1; return; } const homeDir = homedir(); const ui = new ClackUI(); // 1. Extensions : ClaudeCode est embarquée et toujours activée, les autres viennent de ~/.magiccode/extensions const registry = new ModeRegistry(); const userExtensions = await discoverUserExtensions(join(homeDir, ".magiccode", "extensions")); await activateExtensions(registry, [claudeCodeExtension, ...userExtensions], (m) => ui.error(m)); // 2. Configuration const config = await resolveConfig({ cwd, homeDir, flags: { mode: values.mode, model: values.model }, }); /** * Charge les modèles disponibles auprès de tous les providers, puis demande à l'utilisateur * d'en choisir un. Retourne null si rien n'est disponible ou si l'utilisateur annule. */ const pickModel = async (current?: string): Promise => { const { choices, errors } = await ui.withSpinner( "Chargement des modèles disponibles…", () => loadModelChoices({ baseDir: cwd }), "Modèles chargés", ); for (const e of errors) ui.warn(e); if (choices.length === 0) { ui.error( "Aucun modèle disponible. Renseignez ./btp_AI.json (SAP AI Core) ou définissez ANTHROPIC_API_KEY.", ); return null; } return ui.select("Modèle", choices, current); }; const startSession = async (modeId: string, modelSpec: string): Promise => { const mode = registry.getMode(modeId); if (!mode) { const ids = registry.listModes().map((m) => m.id).join(", "); throw new Error(`Mode inconnu : "${modeId}" (disponibles : ${ids})`); } const init = await mode.initialize({ cwd, homeDir, log: (m) => ui.info(m), warn: (m) => ui.warn(m), }); return createSession({ provider: createProvider(modelSpec, { baseDir: cwd }), init, ui, cwd }); }; ui.intro("MagicCode"); // Dossier de lancement : base de tous les chemins relatifs (fichiers lus, créés, configuration) ui.info(`Dossier de travail : ${cwd}`); // 3. Modèle de la session : explicite (flag, variable d'environnement, config) sinon choisi par l'utilisateur let modelSpec = config.model; if (!modelSpec) { const picked = await pickModel(); if (!picked) { ui.outro("Aucun modèle choisi"); process.exitCode = 1; return; } modelSpec = picked; } let modeId = config.mode; let session: AgentSession; try { session = await startSession(modeId, modelSpec); } catch (err) { ui.error((err as Error).message); process.exitCode = 1; return; } ui.info(`Mode : ${modeId} · Modèle : ${modelSpec}`); // 4. Boucle de conversation for (;;) { const input = await ui.readInput(); if (input === null || input === "/exit") break; if (!input) continue; let prompt = input; const skillCall = parseSkillInvocation(input); if (skillCall) { // Appel direct d'un skill de l'agent chargé : §nom [arguments] if (!skillCall.name) { ui.info( session.skills.length > 0 ? ["Skills disponibles :", ...session.skills.map((s) => ` §${s.name} — ${s.description}`)].join("\n") : "Aucun skill disponible pour ce mode.", ); continue; } const skill = findSkill(session.skills, skillCall.name); if (!skill) { const names = session.skills.map((s) => `§${s.name}`).join(", ") || "aucun"; ui.error(`Skill inconnu : §${skillCall.name} (disponibles : ${names})`); continue; } try { prompt = await skill.invoke(skillCall.args); } catch (err) { ui.error(`Échec du lancement du skill §${skill.name} : ${(err as Error).message}`); continue; } ui.info(`Skill lancé : §${skill.name}`); } else if (input.startsWith("/")) { const [name = "", ...rest] = input.slice(1).split(" "); const args = rest.join(" "); if (name === "help") { const custom = session.commands.map( (c) => `/${c.name}${c.argumentHint ? ` ${c.argumentHint}` : ""} — ${c.description}`, ); const skills = session.skills.map((s) => `§${s.name} — ${s.description}`); ui.info([HELP, ...custom, ...skills].join("\n")); continue; } if (name === "clear") { session.messages.length = 0; ui.info("Conversation réinitialisée"); continue; } if (name === "mode") { const choice = await ui.select( "Mode de fonctionnement", registry.listModes().map((m) => ({ value: m.id, label: m.name, hint: m.description })), modeId, ); if (choice) { try { session = await startSession(choice, modelSpec); modeId = choice; ui.info(`Mode actif : ${modeId} (conversation réinitialisée)`); } catch (err) { ui.error((err as Error).message); } } continue; } if (name === "model") { // Recharge la liste (les déploiements ont pu changer) ; la conversation en cours est conservée. const choice = await pickModel(modelSpec); if (choice && choice !== modelSpec) { try { session.provider = createProvider(choice, { baseDir: cwd }); modelSpec = choice; ui.info(`Modèle actif : ${modelSpec}`); } catch (err) { ui.error((err as Error).message); } } continue; } const custom = session.commands.find((c) => c.name === name); if (!custom) { ui.error(`Commande inconnue : /${name}`); continue; } prompt = custom.prompt(args); } try { await runTurn(session, prompt); } catch (err) { ui.error((err as Error).message); } } ui.outro("À bientôt"); } main().catch((err) => { console.error(err); process.exit(1); });