233 lines
7.4 KiB
TypeScript
233 lines
7.4 KiB
TypeScript
|
|
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 <id> Mode de fonctionnement (défaut : claudecode)
|
||
|
|
--model <spec> 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<void> {
|
||
|
|
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<string | null> => {
|
||
|
|
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<AgentSession> => {
|
||
|
|
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);
|
||
|
|
});
|