From bfcd56cd367143e7b1f9da1375795618837359c4 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sun, 11 Oct 2026 02:11:20 +0200 Subject: [PATCH] Initial commit --- .gitignore | 5 + README.md | 93 + btp_AI_tpl.json | 8 + package.json | 22 + packages/cli/package.json | 26 + packages/cli/src/config.ts | 35 + packages/cli/src/index.ts | 232 ++ packages/cli/src/launch-dir.test.ts | 22 + packages/cli/src/launch-dir.ts | 17 + packages/cli/tsconfig.json | 4 + packages/cli/tsup.config.ts | 11 + packages/core/package.json | 8 + packages/core/src/agent.test.ts | 103 + packages/core/src/agent.ts | 153 ++ packages/core/src/extensions.ts | 48 + packages/core/src/index.ts | 5 + packages/core/src/registry.ts | 20 + packages/core/src/skills.test.ts | 31 + packages/core/src/skills.ts | 21 + packages/core/src/types.ts | 152 ++ packages/core/tsconfig.json | 4 + packages/ext-claudecode/package.json | 18 + packages/ext-claudecode/src/commands.ts | 83 + packages/ext-claudecode/src/frontmatter.ts | 35 + packages/ext-claudecode/src/index.ts | 12 + packages/ext-claudecode/src/lot2.test.ts | 148 ++ packages/ext-claudecode/src/memory.ts | 92 + packages/ext-claudecode/src/mode.test.ts | 64 + packages/ext-claudecode/src/mode.ts | 74 + packages/ext-claudecode/src/permissions.ts | 116 + packages/ext-claudecode/src/report.ts | 67 + packages/ext-claudecode/src/settings.ts | 71 + packages/ext-claudecode/src/skills.ts | 119 + packages/ext-claudecode/src/tools/bash.ts | 40 + packages/ext-claudecode/src/tools/edit.ts | 36 + packages/ext-claudecode/src/tools/glob.ts | 62 + packages/ext-claudecode/src/tools/grep.ts | 161 ++ packages/ext-claudecode/src/tools/helpers.ts | 40 + packages/ext-claudecode/src/tools/index.ts | 10 + .../ext-claudecode/src/tools/paths.test.ts | 37 + packages/ext-claudecode/src/tools/read.ts | 31 + .../src/tools/search-tools.test.ts | 73 + packages/ext-claudecode/src/tools/walk.ts | 65 + packages/ext-claudecode/src/tools/write.ts | 28 + packages/ext-claudecode/tsconfig.json | 4 + packages/providers/package.json | 12 + packages/providers/src/anthropic.ts | 73 + packages/providers/src/index.ts | 114 + packages/providers/src/ollama.ts | 8 + packages/providers/src/openai.ts | 8 + packages/providers/src/sapaicore/auth.ts | 68 + packages/providers/src/sapaicore/claude.ts | 87 + packages/providers/src/sapaicore/config.ts | 56 + .../providers/src/sapaicore/deployments.ts | 129 + packages/providers/src/sapaicore/http.ts | 44 + packages/providers/src/sapaicore/index.ts | 106 + packages/providers/src/sapaicore/openai.ts | 80 + .../providers/src/sapaicore/sapaicore.test.ts | 78 + packages/providers/tsconfig.json | 4 + packages/ui/package.json | 12 + packages/ui/src/clack-ui.ts | 100 + packages/ui/src/index.ts | 1 + packages/ui/tsconfig.json | 4 + pnpm-lock.yaml | 2164 +++++++++++++++++ pnpm-workspace.yaml | 2 + tsconfig.base.json | 14 + 66 files changed, 5670 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 btp_AI_tpl.json create mode 100644 package.json create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/config.ts create mode 100644 packages/cli/src/index.ts create mode 100644 packages/cli/src/launch-dir.test.ts create mode 100644 packages/cli/src/launch-dir.ts create mode 100644 packages/cli/tsconfig.json create mode 100644 packages/cli/tsup.config.ts create mode 100644 packages/core/package.json create mode 100644 packages/core/src/agent.test.ts create mode 100644 packages/core/src/agent.ts create mode 100644 packages/core/src/extensions.ts create mode 100644 packages/core/src/index.ts create mode 100644 packages/core/src/registry.ts create mode 100644 packages/core/src/skills.test.ts create mode 100644 packages/core/src/skills.ts create mode 100644 packages/core/src/types.ts create mode 100644 packages/core/tsconfig.json create mode 100644 packages/ext-claudecode/package.json create mode 100644 packages/ext-claudecode/src/commands.ts create mode 100644 packages/ext-claudecode/src/frontmatter.ts create mode 100644 packages/ext-claudecode/src/index.ts create mode 100644 packages/ext-claudecode/src/lot2.test.ts create mode 100644 packages/ext-claudecode/src/memory.ts create mode 100644 packages/ext-claudecode/src/mode.test.ts create mode 100644 packages/ext-claudecode/src/mode.ts create mode 100644 packages/ext-claudecode/src/permissions.ts create mode 100644 packages/ext-claudecode/src/report.ts create mode 100644 packages/ext-claudecode/src/settings.ts create mode 100644 packages/ext-claudecode/src/skills.ts create mode 100644 packages/ext-claudecode/src/tools/bash.ts create mode 100644 packages/ext-claudecode/src/tools/edit.ts create mode 100644 packages/ext-claudecode/src/tools/glob.ts create mode 100644 packages/ext-claudecode/src/tools/grep.ts create mode 100644 packages/ext-claudecode/src/tools/helpers.ts create mode 100644 packages/ext-claudecode/src/tools/index.ts create mode 100644 packages/ext-claudecode/src/tools/paths.test.ts create mode 100644 packages/ext-claudecode/src/tools/read.ts create mode 100644 packages/ext-claudecode/src/tools/search-tools.test.ts create mode 100644 packages/ext-claudecode/src/tools/walk.ts create mode 100644 packages/ext-claudecode/src/tools/write.ts create mode 100644 packages/ext-claudecode/tsconfig.json create mode 100644 packages/providers/package.json create mode 100644 packages/providers/src/anthropic.ts create mode 100644 packages/providers/src/index.ts create mode 100644 packages/providers/src/ollama.ts create mode 100644 packages/providers/src/openai.ts create mode 100644 packages/providers/src/sapaicore/auth.ts create mode 100644 packages/providers/src/sapaicore/claude.ts create mode 100644 packages/providers/src/sapaicore/config.ts create mode 100644 packages/providers/src/sapaicore/deployments.ts create mode 100644 packages/providers/src/sapaicore/http.ts create mode 100644 packages/providers/src/sapaicore/index.ts create mode 100644 packages/providers/src/sapaicore/openai.ts create mode 100644 packages/providers/src/sapaicore/sapaicore.test.ts create mode 100644 packages/providers/tsconfig.json create mode 100644 packages/ui/package.json create mode 100644 packages/ui/src/clack-ui.ts create mode 100644 packages/ui/src/index.ts create mode 100644 packages/ui/tsconfig.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 tsconfig.base.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..917b463 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +*.log +.env +btp_AI.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..26407bb --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# MagicCode + +Assistant IA en ligne de commande. Le **mode** (comportement de l'agent) vient d'une **extension**, +le **LLM** se choisit indépendamment, l'interface utilise **Clack**. + +## Démarrer + +```bash +pnpm install +cp btp_AI_tpl.json btp_AI.json # puis renseigner url, basic, serviceurls.AI_API_URL +pnpm dev # mode claudecode ; le modèle est choisi dans la liste au démarrage +pnpm dev -- --model sapaicore:anthropic--claude-4.5-sonnet +pnpm dev -- --model anthropic:opus # API Anthropic directe (ANTHROPIC_API_KEY) +pnpm test # test de la boucle agent (provider factice) +pnpm build && node packages/cli/dist/index.js +``` + +## Lancer MagicCode depuis un autre dossier + +`pnpm dev` s'exécute depuis la racine du dépôt : le dossier de travail est alors cette racine (`./btp_AI.json`, `./CLAUDE.md`… y sont cherchés). +Pour utiliser MagicCode sur un autre projet `./X`, lancez-le depuis `./X` : + +```bash +pnpm build +cd packages/cli && pnpm link --global # une fois : installe la commande `magiccode` +cd ~/X && magiccode # le dossier de travail est ~/X +# ou sans installer : cd ~/X && node /chemin/vers/magiccode/packages/cli/dist/index.js +# ou en développement : MAGICCODE_CWD=~/X pnpm --dir /chemin/vers/magiccode dev +``` + +Pourquoi pas `pnpm --filter … dev` : pnpm exécute alors le script depuis `packages/cli`, pas depuis le dossier où vous avez tapé la commande. MagicCode +tient compte de `MAGICCODE_CWD`, puis de `INIT_CWD` (dossier où pnpm/npm a été lancé), puis de `process.cwd()`. + +## Architecture + +| Paquet | Rôle | +| --- | --- | +| `core` | Contrats (`LLMProvider`, `AgentMode`, `Extension`, `Tool`, `AgentUI`), boucle agent, registre, chargement des extensions | +| `providers` | Adaptateurs LLM : `sapaicore` (premier provider, voir ci-dessous), `anthropic` (direct), `openai` et `ollama` (à faire) | +| `ui` | Interface Clack (saisies, spinner, confirmations) | +| `ext-claudecode` | Mode par défaut : CLAUDE.md, dossiers `.claude` (settings, commandes, skills) + outils Read/Glob/Grep/Write/Edit/Bash | +| `cli` | Point d'entrée : configuration, extensions, boucle de conversation | + +Résolution du mode et du modèle (le modèle n'a pas de défaut : s'il n'est pas défini, il est demandé) : flags CLI > variables `MAGICCODE_MODE` / `MAGICCODE_MODEL` +> `.magiccode/config.json` (projet) > `~/.magiccode/config.json` > défauts. + +Extensions tierces : `~/.magiccode/extensions//index.js` (export default d'un objet `Extension`). + +## Dossier de base + +Le dossier où MagicCode est lancé (ex. `./X`) est la base de **tous** les chemins relatifs : fichiers lus, créés ou modifiés par les outils +(Read, Write, Edit, Glob, Grep, Bash), `./btp_AI.json`, `.magiccode/config.json`, `CLAUDE.md` et `.claude/`. Un chemin complet est utilisé tel quel +(`~/…` désigne le dossier personnel). Ce dossier est capturé une fois au démarrage, affiché (« Dossier de travail : … ») et transmis aux outils et aux providers ; +les règles de permissions (`Read(./.env)`, `Edit(src/**)`) sont elles aussi relatives à ce dossier. Seuls `~/.claude` et `~/.magiccode` (config et extensions utilisateur) sont lus depuis le dossier personnel. + +## Skills : appel direct avec `§` + +Quel que soit le mode chargé, `§nom_du_skill [arguments]` lance un skill de l'agent (ex. `§sk_test`) ; `§` seul liste les skills disponibles. +Chaque mode expose ses skills via `ModeInitResult.skills` (contrat `AgentSkill` : `name`, `description`, `invoke(args)`), ce qui +rend l'appel indépendant du format propre à l'agent. `invoke` retourne le prompt envoyé au LLM. En mode ClaudeCode, ce prompt contient +les instructions du `SKILL.md` (avec `$ARGUMENTS` / `ARGUMENTS:`), sans passer par l'outil `Skill` du modèle. Les extensions Copilot, opencode, etc. +n'ont qu'à renvoyer leurs propres skills sous cette forme. + +## Provider SAP AI Core / Generative AI Hub + +Configuration : `./btp_AI.json` (ou `$MAGICCODE_BTP_CONFIG`), modèle fourni dans `btp_AI_tpl.json`. +Ne jamais versionner ce fichier (il est dans `.gitignore`). + +1. OAuth client_credentials : `GET {url}/oauth/token` avec `Authorization: Basic {basic}` (token mis en cache, renouvelé sur expiration ou HTTP 401). +2. Découverte : `GET {AI_API_URL}/v2/lm/deployments` (header `AI-Resource-Group`, `default` sauf `resourceGroup` dans le JSON). +3. Appel du modèle sur son `deploymentUrl` selon la famille : Claude → `/converse` (Bedrock), GPT → `/chat/completions` (Azure OpenAI). Les autres familles (Gemini, embeddings…) sont ignorées. + +Choix du modèle : au démarrage (sauf si `--model`, `MAGICCODE_MODEL` ou une config le définit), MagicCode interroge +tous les providers (déploiements SAP AI Core, et API Anthropic si `ANTHROPIC_API_KEY` est définie) et demande de choisir +le modèle de la session. `/model` rouvre cette liste en cours de session (la conversation est conservée). +Un provider injoignable donne un avertissement sans bloquer les autres. Spécification explicite : `sapaicore:`. + +## Extension ClaudeCode : dossiers `.claude` (lot 2) + +Les dossiers `.claude` sont lus dans cet ordre (le plus spécifique l'emporte) : `~/.claude`, puis ceux de la racine jusqu'au dossier courant. + +- **`settings.json` / `settings.local.json`** : `permissions.allow|ask|deny` au format Claude Code (`Bash(npm run test:*)`, `Edit(src/**)`, `Read(./.env)`, `Write`) et `env` (transmis à Bash). Priorité deny > ask > allow. Pour Bash, une règle allow ne couvre une commande composée (`&&`, `;`, `|`) que si chaque partie est autorisée, et jamais une commande avec redirection ou substitution (`>`, `$(…)`, backticks). Les règles deny sont du « meilleur effort » : elles ne remplacent pas un vrai bac à sable. +- **`commands/**/*.md`** : slash-commandes. `$ARGUMENTS`, `$1`…`$9` ; en-tête `description` et `argument-hint`. Un sous-dossier préfixe le nom (`commands/git/commit.md` → `/git:commit`). Les noms `help`, `clear`, `mode`, `model`, `exit` sont réservés. +- **`skills//SKILL.md`** : en-tête `name` et `description`. Seule la liste est mise dans le prompt ; le contenu est chargé à la demande par l'outil `Skill`. + +## Reste à faire + +- Provider SAP AI Core : streaming (actuellement une réponse complète par appel), autres familles (Gemini), validation sur un vrai service + +- `ext-claudecode` : lots 1 et 2 terminés (Glob/Grep en JS pur sans ripgrep ; à l'initialisation, ce qui est trouvé et chargé est listé, sinon un avertissement). Pas encore gérés dans les commandes : `allowed-tools`, `model`, `@fichier`, `!`commande`` +- `ext-claudecode` lot 3 : TodoWrite, WebFetch, sous-agents, hooks, MCP +- Providers OpenAI et Ollama +- Sessions (reprise), compaction du contexte, annulation (AbortSignal) depuis l'UI diff --git a/btp_AI_tpl.json b/btp_AI_tpl.json new file mode 100644 index 0000000..d11632f --- /dev/null +++ b/btp_AI_tpl.json @@ -0,0 +1,8 @@ +{ + "url": "https://.authentication..hana.ondemand.com", + "basic": "", + "serviceurls": { + "AI_API_URL": "https://api.ai.<...>.ml.hana.ondemand.com" + }, + "resourceGroup": "default" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..98b616c --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "magiccode-monorepo", + "private": true, + "type": "module", + "packageManager": "pnpm@9.12.0", + "engines": { + "node": ">=20" + }, + "scripts": { + "dev": "tsx packages/cli/src/index.ts", + "build": "pnpm --filter @magiccode/cli build", + "typecheck": "pnpm -r typecheck", + "test": "vitest run" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsup": "^8.3.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0", + "vitest": "^2.1.0" + } +} diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..93c9334 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,26 @@ +{ + "name": "@magiccode/cli", + "version": "0.1.0", + "type": "module", + "bin": { + "magiccode": "dist/index.js" + }, + "scripts": { + "dev": "tsx src/index.ts", + "build": "tsup", + "typecheck": "tsc -p ." + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.40.0", + "@clack/prompts": "^0.9.0", + "@magiccode/core": "workspace:*", + "@magiccode/ext-claudecode": "workspace:*", + "@magiccode/providers": "workspace:*", + "@magiccode/ui": "workspace:*", + "ignore": "^6.0.2", + "picomatch": "^3.0.1" + }, + "devDependencies": { + "@types/picomatch": "^3.0.1" + } +} diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts new file mode 100644 index 0000000..3e43316 --- /dev/null +++ b/packages/cli/src/config.ts @@ -0,0 +1,35 @@ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; + +export interface MagicCodeConfig { + mode?: string; + model?: string; +} + +export const DEFAULT_MODE = "claudecode"; // mode par défaut : extension ClaudeCode + +async function readJson(path: string): Promise { + try { + return JSON.parse(await readFile(path, "utf8")) as MagicCodeConfig; + } catch { + return {}; + } +} + +/** Priorité : flags CLI > variables d'environnement > config projet > config utilisateur > défauts. */ +export async function resolveConfig(opts: { + cwd: string; + homeDir: string; + flags: MagicCodeConfig; +}): Promise<{ mode: string; model?: string }> { + const user = await readJson(join(opts.homeDir, ".magiccode", "config.json")); + const project = await readJson(join(opts.cwd, ".magiccode", "config.json")); + const env: MagicCodeConfig = { + mode: process.env.MAGICCODE_MODE, + model: process.env.MAGICCODE_MODEL, + }; + const pick = (k: keyof MagicCodeConfig): string | undefined => + opts.flags[k] ?? env[k] ?? project[k] ?? user[k]; + // Pas de modèle par défaut : s'il n'est pas défini explicitement, l'utilisateur le choisit au démarrage. + return { mode: pick("mode") ?? DEFAULT_MODE, model: pick("model") }; +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 0000000..596097d --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,232 @@ +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); +}); diff --git a/packages/cli/src/launch-dir.test.ts b/packages/cli/src/launch-dir.test.ts new file mode 100644 index 0000000..f30dfee --- /dev/null +++ b/packages/cli/src/launch-dir.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { resolveLaunchDir } from "./launch-dir"; + +describe("resolveLaunchDir", () => { + const pkg = "/repo/packages/cli"; + + it("lancement direct : process.cwd()", () => { + expect(resolveLaunchDir({}, "/home/u/X")).toBe("/home/u/X"); + }); + + it("via pnpm : INIT_CWD l'emporte sur le dossier du paquet", () => { + expect(resolveLaunchDir({ INIT_CWD: "/repo" }, pkg)).toBe("/repo"); + }); + + it("MAGICCODE_CWD l'emporte sur INIT_CWD", () => { + expect(resolveLaunchDir({ MAGICCODE_CWD: "/work/X", INIT_CWD: "/repo" }, pkg)).toBe("/work/X"); + }); + + it("MAGICCODE_CWD relatif : résolu depuis le dossier courant", () => { + expect(resolveLaunchDir({ MAGICCODE_CWD: "../X" }, "/home/u/Y")).toBe("/home/u/X"); + }); +}); diff --git a/packages/cli/src/launch-dir.ts b/packages/cli/src/launch-dir.ts new file mode 100644 index 0000000..f701a04 --- /dev/null +++ b/packages/cli/src/launch-dir.ts @@ -0,0 +1,17 @@ +import { resolve } from "node:path"; + +/** + * Dossier de lancement de MagicCode = base de tous les chemins relatifs. + * + * pnpm/npm exécutent les scripts depuis le dossier du paquet (ex. packages/cli) et non depuis le dossier + * où la commande a été tapée : process.cwd() n'est alors pas le bon dossier. Priorité : + * 1. MAGICCODE_CWD : choix explicite ; + * 2. INIT_CWD : dossier où pnpm/npm a été lancé ; + * 3. process.cwd() : lancement direct (binaire `magiccode`, `node …/dist/index.js`). + */ +export function resolveLaunchDir( + env: NodeJS.ProcessEnv = process.env, + cwd: string = process.cwd(), +): string { + return resolve(cwd, env.MAGICCODE_CWD || env.INIT_CWD || "."); +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 0000000..564a599 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"] +} diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts new file mode 100644 index 0000000..f97c4b9 --- /dev/null +++ b/packages/cli/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + target: "node20", + clean: true, + // Les paquets du workspace sont embarqués dans le bundle ; les dépendances npm restent externes. + noExternal: [/^@magiccode\//], + banner: { js: "#!/usr/bin/env node" }, +}); diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..fb7d58c --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,8 @@ +{ + "name": "@magiccode/core", + "version": "0.1.0", + "type": "module", + "main": "src/index.ts", + "types": "src/index.ts", + "scripts": { "typecheck": "tsc -p ." } +} diff --git a/packages/core/src/agent.test.ts b/packages/core/src/agent.test.ts new file mode 100644 index 0000000..9baf23d --- /dev/null +++ b/packages/core/src/agent.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from "vitest"; +import { createSession, runTurn } from "./agent"; +import type { AgentUI, LLMProvider, PermissionPolicy, StreamEvent, Tool } from "./types"; + +const silentUI: AgentUI = { + onThinking() {}, + onText() {}, + onTextEnd() {}, + onToolCall() {}, + onToolResult() {}, + confirm: async () => true, +}; + +/** Provider factice : 1er appel → demande d'outil, 2e appel → réponse texte. */ +function fakeProvider(toolName = "echo"): LLMProvider { + let call = 0; + return { + id: "fake", + model: "fake", + capabilities: { tools: true }, + async *chat(): AsyncIterable { + call++; + if (call === 1) { + yield { type: "tool_use", id: "t1", name: toolName, input: { value: "salut" } }; + yield { type: "done", stopReason: "tool_use" }; + } else { + yield { type: "text_delta", text: "terminé" }; + yield { type: "done", stopReason: "end_turn" }; + } + }, + }; +} + +describe("runTurn", () => { + it("exécute l'outil demandé puis renvoie la réponse finale", async () => { + const echo: Tool = { + spec: { name: "echo", description: "echo", inputSchema: { type: "object" } }, + permission: "auto", + execute: async (input) => String(input.value), + }; + const session = createSession({ + provider: fakeProvider(), + init: { systemPrompt: "test", tools: [echo] }, + ui: silentUI, + cwd: process.cwd(), + }); + + await runTurn(session, "bonjour"); + + // user, assistant(tool_use), user(tool_result), assistant(texte) + expect(session.messages).toHaveLength(4); + expect(session.messages[2]?.content[0]).toMatchObject({ + type: "tool_result", + content: "salut", + }); + }); +}); + +describe("politique de permissions", () => { + const makeTool = (ran: string[]): Tool => ({ + spec: { name: "danger", description: "d", inputSchema: { type: "object" } }, + permission: "ask", + execute: async () => { + ran.push("x"); + return "ok"; + }, + }); + const policy = (decision: "allow" | "ask" | "deny"): PermissionPolicy => ({ + check: () => ({ decision, reason: "règle de test" }), + }); + + async function run(decision: "allow" | "ask" | "deny", answer: boolean) { + const ran: string[] = []; + const confirms: string[] = []; + const session = createSession({ + provider: fakeProvider("danger"), + init: { systemPrompt: "t", tools: [makeTool(ran)], permissions: policy(decision) }, + ui: { ...silentUI, confirm: async (m) => (confirms.push(m), answer) }, + cwd: process.cwd(), + }); + await runTurn(session, "go"); + return { ran, confirms, session }; + } + + it("allow : exécute sans confirmation", async () => { + const { ran, confirms } = await run("allow", false); + expect(ran).toHaveLength(1); + expect(confirms).toHaveLength(0); + }); + + it("deny : refuse sans demander", async () => { + const { ran, confirms, session } = await run("deny", true); + expect(ran).toHaveLength(0); + expect(confirms).toHaveLength(0); + expect(session.messages[2]?.content[0]).toMatchObject({ type: "tool_result", isError: true }); + }); + + it("ask : demande confirmation même pour un outil automatique", async () => { + const { ran, confirms } = await run("ask", true); + expect(confirms).toHaveLength(1); + expect(ran).toHaveLength(1); + }); +}); diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts new file mode 100644 index 0000000..90949ae --- /dev/null +++ b/packages/core/src/agent.ts @@ -0,0 +1,153 @@ +import type { + AgentSkill, + AgentUI, + ContentBlock, + LLMProvider, + Message, + ModeInitResult, + PermissionPolicy, + SlashCommand, + Tool, +} from "./types"; + +export interface AgentSession { + provider: LLMProvider; + system: string; + tools: Map; + commands: SlashCommand[]; + skills: AgentSkill[]; + permissions?: PermissionPolicy; + env?: Record; + messages: Message[]; + ui: AgentUI; + cwd: string; +} + +export function createSession(opts: { + provider: LLMProvider; + init: ModeInitResult; + ui: AgentUI; + cwd: string; +}): AgentSession { + return { + provider: opts.provider, + system: opts.init.systemPrompt, + tools: new Map(opts.init.tools.map((t) => [t.spec.name, t])), + commands: opts.init.commands ?? [], + skills: opts.init.skills ?? [], + permissions: opts.init.permissions, + env: opts.init.env, + messages: [], + ui: opts.ui, + cwd: opts.cwd, + }; +} + +const MAX_ITERATIONS = 25; + +type ToolUseBlock = Extract; + +/** Un tour complet : message utilisateur → (LLM → outils)* → réponse finale. */ +export async function runTurn( + session: AgentSession, + userText: string, + signal?: AbortSignal, +): Promise { + const { ui } = session; + session.messages.push({ role: "user", content: [{ type: "text", text: userText }] }); + + for (let i = 0; i < MAX_ITERATIONS; i++) { + const assistant: ContentBlock[] = []; + let text = ""; + + ui.onThinking(true); + let first = true; + try { + for await (const ev of session.provider.chat( + { + system: session.system, + messages: session.messages, + tools: [...session.tools.values()].map((t) => t.spec), + }, + signal, + )) { + if (first) { + ui.onThinking(false); + first = false; + } + if (ev.type === "text_delta") { + text += ev.text; + ui.onText(ev.text); + } else if (ev.type === "tool_use") { + assistant.push({ type: "tool_use", id: ev.id, name: ev.name, input: ev.input }); + } + // "done" : la fin du flux suffit, les appels d'outils décident de la suite + } + } finally { + if (first) ui.onThinking(false); + } + + if (text) { + ui.onTextEnd(); + assistant.unshift({ type: "text", text }); + } + session.messages.push({ role: "assistant", content: assistant }); + + const calls = assistant.filter((b): b is ToolUseBlock => b.type === "tool_use"); + if (calls.length === 0) return; + + const results: ContentBlock[] = []; + for (const call of calls) { + results.push(await executeToolCall(session, call, signal)); + } + session.messages.push({ role: "user", content: results }); + } + session.ui.onToolResult("agent", `Arrêt : plus de ${MAX_ITERATIONS} itérations`, true); +} + +async function executeToolCall( + session: AgentSession, + call: ToolUseBlock, + signal?: AbortSignal, +): Promise { + const { ui } = session; + const tool = session.tools.get(call.name); + const result = (content: string, isError = false): ContentBlock => ({ + type: "tool_result", + toolUseId: call.id, + content, + isError, + }); + + if (!tool) return result(`Outil inconnu : ${call.name}`, true); + + const summary = tool.describe?.(call.input) ?? JSON.stringify(call.input).slice(0, 120); + ui.onToolCall(call.name, summary); + + // Les règles du mode (deny > ask > allow) priment sur la permission par défaut de l'outil + const verdict = session.permissions?.check(call.name, call.input, { cwd: session.cwd }); + if (verdict?.decision === "deny") { + const why = verdict.reason ? ` (${verdict.reason})` : ""; + ui.onToolResult(call.name, `Refusé par la configuration${why}`, true); + return result(`Action refusée par les règles de permissions${why}. Ne réessaie pas telle quelle.`, true); + } + const needsConfirm = verdict ? verdict.decision === "ask" : tool.permission === "ask"; + + if (needsConfirm) { + const ok = await ui.confirm(`Autoriser ${call.name} : ${summary} ?`); + if (!ok) { + ui.onToolResult(call.name, "Refusé par l'utilisateur", true); + return result("L'utilisateur a refusé cette action.", true); + } + } + + try { + const output = await tool.execute(call.input, { cwd: session.cwd, signal, env: session.env }); + ui.onToolResult(call.name, output, false); + return result(output); + } catch (err) { + const msg = (err as Error).message; + ui.onToolResult(call.name, msg, true); + return result(msg, true); + } +} diff --git a/packages/core/src/extensions.ts b/packages/core/src/extensions.ts new file mode 100644 index 0000000..ad4f220 --- /dev/null +++ b/packages/core/src/extensions.ts @@ -0,0 +1,48 @@ +import { readdir, stat } from "node:fs/promises"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; +import type { Extension } from "./types"; +import type { ModeRegistry } from "./registry"; + +/** Active les extensions : chacune enregistre ses modes via l'API. */ +export async function activateExtensions( + registry: ModeRegistry, + extensions: Extension[], + onError: (message: string) => void = () => {}, +): Promise { + for (const ext of extensions) { + try { + await ext.activate({ + registerMode: (mode) => registry.registerMode(mode, ext.id), + }); + } catch (err) { + onError(`Extension "${ext.id}" : échec d'activation (${(err as Error).message})`); + } + } +} + +/** + * Découvre les extensions utilisateur : ~/.magiccode/extensions//index.js + * (export default d'un objet Extension). + */ +export async function discoverUserExtensions(dir: string): Promise { + let names: string[]; + try { + names = await readdir(dir); + } catch { + return []; + } + const found: Extension[] = []; + for (const name of names) { + const entry = join(dir, name, "index.js"); + try { + await stat(entry); + const mod = await import(pathToFileURL(entry).href); + const ext = (mod.default ?? mod.extension) as Extension | undefined; + if (ext && typeof ext.activate === "function") found.push(ext); + } catch { + // extension illisible : ignorée + } + } + return found; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..d699a3b --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,5 @@ +export * from "./types"; +export * from "./registry"; +export * from "./extensions"; +export * from "./agent"; +export * from "./skills"; diff --git a/packages/core/src/registry.ts b/packages/core/src/registry.ts new file mode 100644 index 0000000..9ff7f3d --- /dev/null +++ b/packages/core/src/registry.ts @@ -0,0 +1,20 @@ +import type { AgentMode } from "./types"; + +export class ModeRegistry { + private modes = new Map(); + + registerMode(mode: AgentMode, source: string): void { + if (this.modes.has(mode.id)) { + throw new Error(`Mode "${mode.id}" déjà enregistré`); + } + this.modes.set(mode.id, { mode, source }); + } + + getMode(id: string): AgentMode | undefined { + return this.modes.get(id)?.mode; + } + + listModes(): AgentMode[] { + return [...this.modes.values()].map((e) => e.mode); + } +} diff --git a/packages/core/src/skills.test.ts b/packages/core/src/skills.test.ts new file mode 100644 index 0000000..c9a8a2d --- /dev/null +++ b/packages/core/src/skills.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { findSkill, parseSkillInvocation } from "./skills"; +import type { AgentSkill } from "./types"; + +describe("parseSkillInvocation", () => { + it("reconnaît §nom avec ou sans arguments", () => { + expect(parseSkillInvocation("§sk_test")).toEqual({ name: "sk_test", args: "" }); + expect(parseSkillInvocation(" §sk_test lance sur src/ ")).toEqual({ + name: "sk_test", + args: "lance sur src/", + }); + }); + + it("§ seul : nom vide (liste des skills)", () => { + expect(parseSkillInvocation("§")).toEqual({ name: "", args: "" }); + }); + + it("ignore les autres saisies", () => { + expect(parseSkillInvocation("bonjour §sk_test")).toBeNull(); + expect(parseSkillInvocation("/help")).toBeNull(); + }); +}); + +describe("findSkill", () => { + const skills: AgentSkill[] = [{ name: "Sk_Test", description: "d", invoke: () => "p" }]; + it("exact puis insensible à la casse", () => { + expect(findSkill(skills, "Sk_Test")).toBe(skills[0]); + expect(findSkill(skills, "sk_test")).toBe(skills[0]); + expect(findSkill(skills, "autre")).toBeUndefined(); + }); +}); diff --git a/packages/core/src/skills.ts b/packages/core/src/skills.ts new file mode 100644 index 0000000..4280948 --- /dev/null +++ b/packages/core/src/skills.ts @@ -0,0 +1,21 @@ +import type { AgentSkill } from "./types"; + +/** Préfixe d'appel direct d'un skill : "§nom_du_skill [arguments]". */ +export const SKILL_PREFIX = "§"; + +/** + * Reconnaît un appel de skill. Retourne null si la saisie n'en est pas un ; + * `name` vaut "" pour un "§" seul (liste des skills). + */ +export function parseSkillInvocation(input: string): { name: string; args: string } | null { + const text = input.trim(); + if (!text.startsWith(SKILL_PREFIX)) return null; + const m = /^§(\S*)\s*([\s\S]*)$/.exec(text); + return m ? { name: m[1] ?? "", args: (m[2] ?? "").trim() } : null; +} + +/** Nom exact, sinon insensible à la casse. */ +export function findSkill(skills: AgentSkill[], name: string): AgentSkill | undefined { + const lower = name.toLowerCase(); + return skills.find((s) => s.name === name) ?? skills.find((s) => s.name.toLowerCase() === lower); +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts new file mode 100644 index 0000000..348c21e --- /dev/null +++ b/packages/core/src/types.ts @@ -0,0 +1,152 @@ +// ───────────────────────────────────────────────────────────── +// Contrats de MagicCode. Tout le reste du projet en dépend. +// ───────────────────────────────────────────────────────────── + +// ---------- Messages (format neutre, indépendant des providers) ---------- + +export type ContentBlock = + | { type: "text"; text: string } + | { type: "tool_use"; id: string; name: string; input: Record } + | { type: "tool_result"; toolUseId: string; content: string; isError?: boolean }; + +export interface Message { + role: "user" | "assistant"; + content: ContentBlock[]; +} + +// ---------- LLMProvider ---------- + +export interface ToolSpec { + name: string; + description: string; + /** JSON Schema de l'entrée de l'outil. */ + inputSchema: Record; +} + +export interface ChatRequest { + system: string; + messages: Message[]; + tools: ToolSpec[]; + maxTokens?: number; +} + +export type StopReason = "end_turn" | "tool_use" | "max_tokens" | "other"; + +/** Événements de streaming normalisés, identiques pour tous les providers. */ +export type StreamEvent = + | { type: "text_delta"; text: string } + | { type: "tool_use"; id: string; name: string; input: Record } + | { type: "done"; stopReason: StopReason }; + +export interface LLMProvider { + /** ex: "anthropic", "openai", "ollama" */ + readonly id: string; + /** Identifiant réel du modèle chez le fournisseur. */ + readonly model: string; + readonly capabilities: { tools: boolean }; + chat(req: ChatRequest, signal?: AbortSignal): AsyncIterable; +} + +// ---------- Outils ---------- + +export interface ToolContext { + cwd: string; + signal?: AbortSignal; + /** Variables d'environnement additionnelles (ex. `env` de .claude/settings.json). */ + env?: Record; +} + +// ---------- Permissions ---------- + +export interface PermissionDecision { + /** allow : exécuter sans confirmation · ask : demander · deny : refuser sans demander. */ + decision: "allow" | "ask" | "deny"; + reason?: string; +} + +/** Politique fournie par un mode ; retourne null quand aucune règle ne s'applique (comportement par défaut de l'outil). */ +export interface PermissionPolicy { + check(toolName: string, input: Record, ctx: { cwd: string }): PermissionDecision | null; +} + +export interface Tool { + spec: ToolSpec; + /** "ask" : l'UI doit demander confirmation avant exécution. */ + permission: "auto" | "ask"; + /** Résumé court affiché dans l'UI et dans la demande de confirmation. */ + describe?(input: Record): string; + execute(input: Record, ctx: ToolContext): Promise; +} + +// ---------- UI (implémentée par le package `ui`, jamais importée par le core) ---------- + +export interface AgentUI { + onThinking(active: boolean): void; + onText(text: string): void; + onTextEnd(): void; + onToolCall(name: string, summary: string): void; + onToolResult(name: string, output: string, isError: boolean): void; + confirm(message: string): Promise; +} + +// ---------- Modes & extensions ---------- + +/** Commande slash fournie par un mode : elle se transforme en prompt envoyé au LLM. */ +export interface SlashCommand { + name: string; + description: string; + /** Aide à la saisie des arguments, ex. "[message]". */ + argumentHint?: string; + prompt(args: string): string; +} + +/** + * Skill d'un agent, indépendant du format propre à cet agent (SKILL.md de ClaudeCode, etc.). + * L'utilisateur le lance avec "§nom [arguments]", quel que soit le mode chargé. + */ +export interface AgentSkill { + name: string; + description: string; + /** Construit le prompt qui lance le skill (instructions du skill + arguments), envoyé ensuite au LLM. */ + invoke(args: string): string | Promise; +} + +export interface InitContext { + cwd: string; + homeDir: string; + /** Information à l'utilisateur. */ + log(message: string): void; + /** Avertissement à l'utilisateur (non bloquant). */ + warn(message: string): void; +} + +/** Résultat de l'initialisation d'un mode : ce dont l'agent a besoin pour démarrer. */ +export interface ModeInitResult { + systemPrompt: string; + /** Builtins propres au mode. */ + tools: Tool[]; + commands?: SlashCommand[]; + /** Skills de l'agent, appelables par l'utilisateur avec "§nom". */ + skills?: AgentSkill[]; + /** Règles allow/ask/deny du mode (remplacent la permission par défaut des outils quand elles s'appliquent). */ + permissions?: PermissionPolicy; + /** Variables d'environnement transmises aux outils (ex. Bash). */ + env?: Record; +} + +export interface AgentMode { + id: string; + name: string; + description: string; + initialize(ctx: InitContext): Promise; +} + +export interface ExtensionApi { + registerMode(mode: AgentMode): void; +} + +export interface Extension { + id: string; + name: string; + activate(api: ExtensionApi): void | Promise; +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..564a599 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"] +} diff --git a/packages/ext-claudecode/package.json b/packages/ext-claudecode/package.json new file mode 100644 index 0000000..9a092ed --- /dev/null +++ b/packages/ext-claudecode/package.json @@ -0,0 +1,18 @@ +{ + "name": "@magiccode/ext-claudecode", + "version": "0.1.0", + "type": "module", + "main": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "typecheck": "tsc -p ." + }, + "dependencies": { + "@magiccode/core": "workspace:*", + "ignore": "^6.0.2", + "picomatch": "^3.0.1" + }, + "devDependencies": { + "@types/picomatch": "^3.0.1" + } +} diff --git a/packages/ext-claudecode/src/commands.ts b/packages/ext-claudecode/src/commands.ts new file mode 100644 index 0000000..08284f2 --- /dev/null +++ b/packages/ext-claudecode/src/commands.ts @@ -0,0 +1,83 @@ +import { readFile, readdir } from "node:fs/promises"; +import { join } from "node:path"; +import type { SlashCommand } from "@magiccode/core"; +import { parseFrontmatter } from "./frontmatter"; + +/** Noms réservés aux commandes internes de MagicCode (gérées par la CLI). */ +export const RESERVED_COMMANDS = new Set(["help", "clear", "mode", "model", "exit"]); + +async function listMarkdown(dir: string, prefix = ""): Promise<{ name: string; file: string }[]> { + let entries; + try { + entries = await readdir(dir, { withFileTypes: true }); + } catch { + return []; + } + entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); + + const found: { name: string; file: string }[] = []; + for (const e of entries) { + if (e.isDirectory()) { + found.push(...(await listMarkdown(join(dir, e.name), `${prefix}${e.name}:`))); + } else if (e.isFile() && e.name.endsWith(".md")) { + found.push({ name: `${prefix}${e.name.slice(0, -3)}`, file: join(dir, e.name) }); + } + } + return found; +} + +/** $ARGUMENTS = tous les arguments, $1…$9 = arguments séparés par des espaces. */ +export function renderCommand(body: string, args: string): string { + const tokens = args.trim() ? args.trim().split(/\s+/) : []; + let usesArgs = false; + const out = body.replace(/\$ARGUMENTS|\$([1-9])/g, (_m, n: string | undefined) => { + usesArgs = true; + return n ? (tokens[Number(n) - 1] ?? "") : args.trim(); + }); + return !usesArgs && args.trim() ? `${out.trimEnd()}\n\nARGUMENTS: ${args.trim()}` : out; +} + +function fallbackDescription(body: string): string { + const first = body.split("\n").find((l) => l.trim() !== "") ?? ""; + return first.replace(/^#+\s*/, "").trim().slice(0, 80) || "(sans description)"; +} + +/** + * Charge .claude/commands/**.md de chaque dossier .claude (du plus général au plus spécifique : + * une commande du projet remplace celle du même nom au niveau utilisateur). + * Les sous-dossiers donnent des noms préfixés : commands/git/commit.md → /git:commit. + */ +export async function loadCommands( + claudeDirs: string[], +): Promise<{ commands: SlashCommand[]; warnings: string[] }> { + const byName = new Map(); + const warnings: string[] = []; + + for (const dir of claudeDirs) { + for (const { name, file } of await listMarkdown(join(dir, "commands"))) { + if (/\s/.test(name)) { + warnings.push(`Commande ignorée (espace dans le nom) : ${file}`); + continue; + } + if (RESERVED_COMMANDS.has(name)) { + warnings.push(`Commande /${name} ignorée : nom réservé à MagicCode (${file})`); + continue; + } + let raw: string; + try { + raw = await readFile(file, "utf8"); + } catch { + continue; + } + const { data, body } = parseFrontmatter(raw); + // TODO : allowed-tools, model, références @fichier et !`commande` + byName.set(name, { + name, + description: data.description || fallbackDescription(body), + argumentHint: data["argument-hint"] || undefined, + prompt: (args) => renderCommand(body, args), + }); + } + } + return { commands: [...byName.values()], warnings }; +} diff --git a/packages/ext-claudecode/src/frontmatter.ts b/packages/ext-claudecode/src/frontmatter.ts new file mode 100644 index 0000000..baa5d1b --- /dev/null +++ b/packages/ext-claudecode/src/frontmatter.ts @@ -0,0 +1,35 @@ +function unquote(v: string): string { + const m = /^(["'])(.*)\1$/.exec(v); + return m ? (m[2] ?? "") : v; +} + +/** + * Lit un en-tête YAML simple (--- … ---) : paires `clé: valeur`, valeurs entre guillemets, + * et blocs `>` / `|` (lignes indentées jointes par des espaces). Pas de YAML imbriqué. + */ +export function parseFrontmatter(raw: string): { data: Record; body: string } { + const text = raw.replace(/\r\n/g, "\n"); + if (!text.startsWith("---\n")) return { data: {}, body: text }; + const end = text.indexOf("\n---", 4); + if (end === -1) return { data: {}, body: text }; + + const lines = text.slice(4, end).split("\n"); + const body = text.slice(end + 4).replace(/^[ \t]*\n?/, ""); + + const data: Record = {}; + for (let i = 0; i < lines.length; i++) { + const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(lines[i] ?? ""); + if (!m) continue; + let value = (m[2] ?? "").trim(); + if (value === "" || /^[>|][+-]?$/.test(value)) { + const parts: string[] = []; + while (i + 1 < lines.length && /^\s+\S/.test(lines[i + 1] ?? "")) { + i++; + parts.push((lines[i] ?? "").trim()); + } + value = parts.join(" "); + } + data[m[1]!] = unquote(value); + } + return { data, body }; +} diff --git a/packages/ext-claudecode/src/index.ts b/packages/ext-claudecode/src/index.ts new file mode 100644 index 0000000..adda44a --- /dev/null +++ b/packages/ext-claudecode/src/index.ts @@ -0,0 +1,12 @@ +import type { Extension } from "@magiccode/core"; +import { claudeCodeMode } from "./mode"; + +export const claudeCodeExtension: Extension = { + id: "claudecode", + name: "Extension ClaudeCode", + activate(api) { + api.registerMode(claudeCodeMode); + }, +}; + +export default claudeCodeExtension; diff --git a/packages/ext-claudecode/src/lot2.test.ts b/packages/ext-claudecode/src/lot2.test.ts new file mode 100644 index 0000000..3e61735 --- /dev/null +++ b/packages/ext-claudecode/src/lot2.test.ts @@ -0,0 +1,148 @@ +import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { renderCommand } from "./commands"; +import { parseFrontmatter } from "./frontmatter"; +import { claudeCodeMode } from "./mode"; +import { createPermissionPolicy } from "./permissions"; + +describe("frontmatter", () => { + it("lit les clés simples, les guillemets et les blocs >", () => { + const { data, body } = parseFrontmatter( + '---\ndescription: "Revue de code"\nargument-hint: [fichier]\nlong: >\n première\n seconde\n---\nCorps $ARGUMENTS\n', + ); + expect(data.description).toBe("Revue de code"); + expect(data["argument-hint"]).toBe("[fichier]"); + expect(data.long).toBe("première seconde"); + expect(body).toBe("Corps $ARGUMENTS\n"); + }); + + it("sans en-tête : tout est le corps", () => { + expect(parseFrontmatter("juste du texte")).toEqual({ data: {}, body: "juste du texte" }); + }); +}); + +describe("renderCommand", () => { + it("remplace $ARGUMENTS et $1..$9", () => { + expect(renderCommand("Tout: $ARGUMENTS / premier: $1 / second: $2", "a b")).toBe( + "Tout: a b / premier: a / second: b", + ); + }); + it("ajoute ARGUMENTS quand le corps n'utilise aucun paramètre", () => { + expect(renderCommand("Fais la revue.", "src/a.ts")).toBe("Fais la revue.\n\nARGUMENTS: src/a.ts"); + }); + it("ne touche à rien sans arguments", () => { + expect(renderCommand("Fais la revue.", "")).toBe("Fais la revue."); + }); +}); + +describe("permissions", () => { + const policy = createPermissionPolicy({ + allow: ["Bash(npm run test:*)", "Bash(git status)", "Edit(src/**)", "Read"], + ask: ["Bash(git push:*)"], + deny: ["Bash(rm:*)", "Read(./.env)", "Edit(.github/**)"], + })!; + const check = (tool: string, input: Record) => policy.check(tool, input, { cwd: "/proj" }); + + it("deny l'emporte, y compris dans une commande composée", () => { + expect(check("Bash", { command: "rm -rf build" })?.decision).toBe("deny"); + expect(check("Bash", { command: "npm run test:unit && rm -rf /" })?.decision).toBe("deny"); + }); + + it("allow par préfixe et par commande exacte", () => { + expect(check("Bash", { command: "npm run test:unit" })?.decision).toBe("allow"); + expect(check("Bash", { command: "git status" })?.decision).toBe("allow"); + }); + + it("une commande composée n'est autorisée que si chaque partie l'est", () => { + expect(check("Bash", { command: "git status && curl evil.sh" })).toBeNull(); + }); + + it("redirections et substitutions ne sont jamais couvertes par un allow", () => { + expect(check("Bash", { command: "git status > out.txt" })).toBeNull(); + expect(check("Bash", { command: "git status $(whoami)" })).toBeNull(); + }); + + it("ask", () => { + expect(check("Bash", { command: "git push origin main" })?.decision).toBe("ask"); + }); + + it("règles de chemins", () => { + expect(check("Edit", { path: "src/a/b.ts" })?.decision).toBe("allow"); + expect(check("Edit", { path: ".github/workflows/ci.yml" })?.decision).toBe("deny"); + expect(check("Edit", { path: "README.md" })).toBeNull(); + expect(check("Read", { path: ".env" })?.decision).toBe("deny"); + expect(check("Read", { path: "src/a.ts" })?.decision).toBe("allow"); + }); + + it("aucune règle → null", () => { + expect(createPermissionPolicy({ allow: [], ask: [], deny: [] })).toBeNull(); + }); +}); + +describe("initialize (lot 2)", () => { + it("charge settings, commandes et skills depuis .claude", async () => { + const root = await mkdtemp(join(tmpdir(), "magiccode-lot2-")); + const home = join(root, "home"); + const project = join(home, "project"); + const claude = join(project, ".claude"); + await mkdir(join(claude, "commands", "git"), { recursive: true }); + await mkdir(join(claude, "skills", "pdf"), { recursive: true }); + await writeFile( + join(claude, "settings.json"), + JSON.stringify({ permissions: { deny: ["Bash(rm:*)"] }, env: { MA_VAR: "1" } }), + ); + await writeFile( + join(claude, "commands", "review.md"), + "---\ndescription: Revue\nargument-hint: [fichier]\n---\nRevois $ARGUMENTS\n", + ); + await writeFile(join(claude, "commands", "git", "commit.md"), "# Commit\nFais un commit.\n"); + await writeFile(join(claude, "commands", "help.md"), "réservé"); + await writeFile( + join(claude, "skills", "pdf", "SKILL.md"), + "---\nname: pdf\ndescription: Manipuler des PDF\n---\nUtilise pypdf.\n", + ); + + const logs: string[] = []; + const warns: string[] = []; + const init = await claudeCodeMode.initialize({ + cwd: project, + homeDir: home, + log: (m) => logs.push(m), + warn: (m) => warns.push(m), + }); + + // commandes + expect(init.commands?.map((c) => c.name).sort()).toEqual(["git:commit", "review"]); + const review = init.commands?.find((c) => c.name === "review"); + expect(review?.description).toBe("Revue"); + expect(review?.argumentHint).toBe("[fichier]"); + expect(review?.prompt("a.ts")).toContain("Revois a.ts"); + expect(warns.some((w) => w.includes("/help"))).toBe(true); // nom réservé + + // skills + expect(init.systemPrompt).toContain("pdf : Manipuler des PDF"); + const skillTool = init.tools.find((t) => t.spec.name === "Skill"); + expect(skillTool).toBeDefined(); + expect(await skillTool!.execute({ skill: "pdf" }, { cwd: project })).toContain("Utilise pypdf."); + + // appel direct "§pdf [arguments]" + const direct = init.skills?.find((k) => k.name === "pdf"); + expect(direct?.description).toBe("Manipuler des PDF"); + const prompt = await direct!.invoke("sur a.pdf"); + expect(prompt).toContain('lance explicitement le skill "pdf"'); + expect(prompt).toContain("Utilise pypdf."); + expect(prompt).toContain("ARGUMENTS: sur a.pdf"); + + // permissions et env + expect(init.permissions?.check("Bash", { command: "rm -rf x" }, { cwd: project })?.decision).toBe("deny"); + expect(init.env).toEqual({ MA_VAR: "1" }); + + // rapport + const report = logs.join("\n"); + expect(report).toContain("Chargé depuis .claude"); + expect(report).toContain("/review"); + expect(report).toContain("skills : pdf"); + }); +}); diff --git a/packages/ext-claudecode/src/memory.ts b/packages/ext-claudecode/src/memory.ts new file mode 100644 index 0000000..732976a --- /dev/null +++ b/packages/ext-claudecode/src/memory.ts @@ -0,0 +1,92 @@ +import { readFile, readdir, stat } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; + +export interface MemoryFile { + path: string; + content: string; +} + +export interface ClaudeDir { + path: string; + /** Éléments reconnus et exploités, ex. "settings.json", "commands/", "skills/". */ + entries: string[]; + /** Autres éléments présents mais NON exploités (ex. "agents/", "rules/", ou un lien symbolique). */ + unsupported: string[]; +} + +export interface ClaudeConfig { + /** CLAUDE.md chargés, du plus général au plus spécifique. */ + memoryFiles: MemoryFile[]; + /** Dossiers .claude trouvés (global, puis de la racine jusqu'au dossier courant). */ + claudeDirs: ClaudeDir[]; +} + +const KNOWN_FILES = ["settings.json", "settings.local.json", "CLAUDE.md"]; +const KNOWN_DIRS = ["commands", "skills"]; +const IGNORED_NAMES = new Set([".DS_Store", "Thumbs.db"]); + +async function tryRead(path: string): Promise { + try { + return { path, content: await readFile(path, "utf8") }; + } catch { + return null; + } +} + +async function inspectClaudeDir(path: string): Promise { + try { + if (!(await stat(path)).isDirectory()) return null; + const dirents = await readdir(path, { withFileTypes: true }); + const files = dirents.filter((d) => d.isFile() && KNOWN_FILES.includes(d.name)).map((d) => d.name); + const dirs = dirents.filter((d) => d.isDirectory() && KNOWN_DIRS.includes(d.name)).map((d) => d.name); + const known = new Set([...files, ...dirs]); + const unsupported = dirents + .filter((d) => !known.has(d.name) && !IGNORED_NAMES.has(d.name)) + .map((d) => (d.isDirectory() ? `${d.name}/` : d.name)); + return { path, entries: [...files, ...dirs.map((n) => `${n}/`)], unsupported }; + } catch { + return null; + } +} + +/** Dossiers de la racine jusqu'à cwd (le plus spécifique en dernier). */ +function directoryChain(cwd: string): string[] { + const chain: string[] = []; + let dir = resolve(cwd); + for (;;) { + chain.unshift(dir); + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + return chain; +} + +/** + * Découvre la configuration ClaudeCode : + * - CLAUDE.md : global (~/.claude/CLAUDE.md), puis, pour chaque dossier de la racine + * à cwd, /CLAUDE.md et /.claude/CLAUDE.md ; + * - dossiers .claude : global (~/.claude), puis /.claude pour chaque dossier de la chaîne. + */ +export async function loadClaudeConfig(cwd: string, homeDir: string): Promise { + const chain = directoryChain(cwd); + + const memoryCandidates = [ + join(homeDir, ".claude", "CLAUDE.md"), + ...chain.flatMap((d) => [join(d, "CLAUDE.md"), join(d, ".claude", "CLAUDE.md")]), + ]; + const memoryFiles: MemoryFile[] = []; + for (const path of new Set(memoryCandidates)) { + const file = await tryRead(path); + if (file) memoryFiles.push(file); + } + + const dirCandidates = [join(homeDir, ".claude"), ...chain.map((d) => join(d, ".claude"))]; + const claudeDirs: ClaudeDir[] = []; + for (const path of new Set(dirCandidates)) { + const dir = await inspectClaudeDir(path); + if (dir) claudeDirs.push(dir); + } + + return { memoryFiles, claudeDirs }; +} diff --git a/packages/ext-claudecode/src/mode.test.ts b/packages/ext-claudecode/src/mode.test.ts new file mode 100644 index 0000000..d197999 --- /dev/null +++ b/packages/ext-claudecode/src/mode.test.ts @@ -0,0 +1,64 @@ +import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { claudeCodeMode } from "./mode"; + +async function setup() { + const root = await mkdtemp(join(tmpdir(), "magiccode-cc-")); + const home = join(root, "home"); + const project = join(home, "project"); + await mkdir(project, { recursive: true }); + const logs: string[] = []; + const warns: string[] = []; + const ctx = { + cwd: project, + homeDir: home, + log: (m: string) => logs.push(m), + warn: (m: string) => warns.push(m), + }; + return { home, project, logs, warns, ctx }; +} + +describe("ClaudeCode initialize", () => { + it("liste les CLAUDE.md et dossiers .claude trouvés", async () => { + const { home, project, logs, warns, ctx } = await setup(); + await mkdir(join(home, ".claude"), { recursive: true }); + await writeFile(join(home, ".claude", "CLAUDE.md"), "global"); + await mkdir(join(project, ".claude", "commands"), { recursive: true }); + await writeFile(join(project, ".claude", "settings.json"), "{}"); + await writeFile(join(project, "CLAUDE.md"), "projet"); + + const init = await claudeCodeMode.initialize(ctx); + + expect(warns).toHaveLength(0); + const report = logs.join("\n"); + expect(report).toContain("~/.claude/CLAUDE.md"); + expect(report).toContain("./CLAUDE.md"); + expect(report).toContain("./.claude (settings.json, commands/)"); + expect(init.systemPrompt).toContain("projet"); + }); + + it("avertit quand aucune configuration n'est trouvée", async () => { + const { logs, warns, ctx } = await setup(); + + await claudeCodeMode.initialize(ctx); + + expect(logs).toHaveLength(0); + expect(warns[0]).toContain("Aucune configuration ClaudeCode"); + }); + + it("signale les éléments .claude présents mais non exploités, y compris dans un dossier parent", async () => { + const { home, project, logs, ctx } = await setup(); + const parentClaude = join(home, ".claude"); // dossier parent du projet + await mkdir(join(parentClaude, "agents"), { recursive: true }); + await mkdir(join(parentClaude, "rules"), { recursive: true }); + await writeFile(join(parentClaude, "settings.json"), "{}"); + await writeFile(join(project, "CLAUDE.md"), "projet"); + + await claudeCodeMode.initialize(ctx); + + const report = logs.join("\n"); + expect(report).toContain("~/.claude (settings.json ; non exploité : agents/, rules/)"); + }); +}); diff --git a/packages/ext-claudecode/src/mode.ts b/packages/ext-claudecode/src/mode.ts new file mode 100644 index 0000000..13e5be1 --- /dev/null +++ b/packages/ext-claudecode/src/mode.ts @@ -0,0 +1,74 @@ +import type { AgentMode } from "@magiccode/core"; +import { loadCommands } from "./commands"; +import { loadClaudeConfig } from "./memory"; +import { createPermissionPolicy } from "./permissions"; +import { formatConfigReport } from "./report"; +import { loadSettings } from "./settings"; +import { createSkillTool, loadSkills, skillsPromptSection, toAgentSkills } from "./skills"; +import { claudeCodeTools } from "./tools"; + +function buildSystemPrompt( + cwd: string, + memory: { path: string; content: string }[], + skillsSection: string, +): string { + const parts = [ + "Tu es MagicCode, un assistant de programmation en ligne de commande.", + "Tu aides l'utilisateur à lire, comprendre et modifier le code de son projet en utilisant tes outils.", + "Réponds dans la langue de l'utilisateur, de façon concise.", + "Lis un fichier avant de le modifier. Ne fais que les modifications demandées.", + "Pour chercher des fichiers ou du contenu, préfère les outils Glob et Grep à Bash.", + `Dossier de travail : ${cwd} (dossier de lancement de MagicCode). Les chemins relatifs, y compris pour créer un fichier, sont résolus depuis ce dossier ; n'utilise un chemin complet que s'il t'est fourni.`, + ]; + for (const file of memory) { + parts.push(`\n# Instructions (${file.path})\n${file.content.trim()}`); + } + if (skillsSection) parts.push(skillsSection); + return parts.join("\n"); +} + +export const claudeCodeMode: AgentMode = { + id: "claudecode", + name: "ClaudeCode", + description: "Fonctionne comme Claude Code (CLAUDE.md, dossier .claude, outils de base)", + async initialize(ctx) { + const config = await loadClaudeConfig(ctx.cwd, ctx.homeDir); + + // Lot 2 : exploitation des dossiers .claude (global, puis de la racine au dossier courant) + const dirs = config.claudeDirs.map((d) => d.path); + const [{ settings, warnings: w1 }, { commands, warnings: w2 }, { skills, warnings: w3 }] = + await Promise.all([loadSettings(dirs), loadCommands(dirs), loadSkills(dirs)]); + for (const w of [...w1, ...w2, ...w3]) ctx.warn(w); + + // Information de l'utilisateur sur ce qui a été trouvé et chargé (ou non) + const report = formatConfigReport(config, ctx.cwd, ctx.homeDir, { + settingsFiles: settings.files, + rules: { + allow: settings.permissions.allow.length, + deny: settings.permissions.deny.length, + ask: settings.permissions.ask.length, + }, + envCount: Object.keys(settings.env).length, + commands: commands.map((c) => c.name), + skills: skills.map((s) => s.name), + }); + if (report) { + ctx.log(report); + } else { + ctx.warn( + "Aucune configuration ClaudeCode n'a été trouvée (ni CLAUDE.md, ni dossier .claude) dans ~/.claude ni dans " + + `les dossiers de la racine jusqu'à ${ctx.cwd}. MagicCode démarre avec ses instructions par défaut.`, + ); + } + + // TODO lot 3 : agents/ (sous-agents), hooks, serveurs MCP ; commandes : allowed-tools, model, @fichier, !`commande`. + return { + systemPrompt: buildSystemPrompt(ctx.cwd, config.memoryFiles, skillsPromptSection(skills)), + tools: skills.length > 0 ? [...claudeCodeTools, createSkillTool(skills)] : claudeCodeTools, + commands, + skills: toAgentSkills(skills), + permissions: createPermissionPolicy(settings.permissions) ?? undefined, + env: Object.keys(settings.env).length > 0 ? settings.env : undefined, + }; + }, +}; diff --git a/packages/ext-claudecode/src/permissions.ts b/packages/ext-claudecode/src/permissions.ts new file mode 100644 index 0000000..1de6b8e --- /dev/null +++ b/packages/ext-claudecode/src/permissions.ts @@ -0,0 +1,116 @@ +import { isAbsolute, relative, resolve, sep } from "node:path"; +import picomatch from "picomatch"; +import type { PermissionPolicy } from "@magiccode/core"; +import type { PermissionRules } from "./settings"; + +interface Rule { + tool: string; + spec?: string; + raw: string; +} + +const RULE_RE = /^([A-Za-z0-9_.-]+)(?:\((.*)\))?$/s; +const PATH_TOOLS = new Set(["Read", "Write", "Edit", "Glob", "Grep"]); +/** Substitutions de commandes, redirections : une règle allow ne peut pas les couvrir de façon sûre. */ +const UNSAFE_SHELL = /\$\(|`|<\(|>/; + +function parseRules(raws: string[]): Rule[] { + const rules: Rule[] = []; + for (const raw of raws) { + const m = RULE_RE.exec(raw.trim()); + if (!m) continue; + const spec = m[2]?.trim(); + rules.push({ tool: m[1]!, spec: spec || undefined, raw }); + } + return rules; +} + +// ----- Bash : la spécification est un préfixe ("npm run test:*"), un motif ("git * --help") ou une commande exacte + +function splitCommands(cmd: string): string[] { + return cmd + .split(/&&|\|\||;|\||\n/) + .map((s) => s.trim()) + .filter(Boolean); +} + +function matchBashSpec(spec: string, sub: string): boolean { + if (spec.endsWith(":*")) return sub.startsWith(spec.slice(0, -2)); + if (spec.includes("*")) { + const source = spec + .split("*") + .map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")) + .join(".*"); + return new RegExp(`^${source}$`).test(sub); + } + return sub === spec; +} + +// ----- Read / Write / Edit / Glob / Grep : la spécification est un motif de chemin relatif au dossier courant + +function targetPath(input: Record, cwd: string): string { + const raw = typeof input.path === "string" && input.path ? input.path : "."; + const abs = resolve(cwd, raw); + const rel = relative(cwd, abs); + const out = rel === "" ? "." : rel.startsWith("..") || isAbsolute(rel) ? abs : rel; + return out.split(sep).join("/"); +} + +function matchPathSpec(spec: string, target: string): boolean { + const s = spec.replace(/^\.\//, "").replace(/\/+$/, ""); + return picomatch([s, `${s}/**`], { dot: true })(target); +} + +// ----- Évaluation + +/** Pour deny/ask : la règle s'applique si une partie de la commande (Bash) ou le chemin correspond. */ +function ruleApplies(rule: Rule, tool: string, input: Record, cwd: string): boolean { + if (rule.tool !== tool) return false; + if (!rule.spec) return true; + if (tool === "Bash") { + const cmd = typeof input.command === "string" ? input.command : ""; + return splitCommands(cmd).some((sub) => matchBashSpec(rule.spec!, sub)); + } + if (PATH_TOOLS.has(tool)) return matchPathSpec(rule.spec, targetPath(input, cwd)); + return false; +} + +/** Pour allow : TOUTES les parties d'une commande Bash doivent être couvertes (et sans redirection ni substitution). */ +function allowApplies(rules: Rule[], tool: string, input: Record, cwd: string): boolean { + const mine = rules.filter((r) => r.tool === tool); + if (mine.length === 0) return false; + if (mine.some((r) => !r.spec)) return true; // ex. "Edit" : tous les usages de l'outil + if (tool === "Bash") { + const cmd = typeof input.command === "string" ? input.command : ""; + if (UNSAFE_SHELL.test(cmd)) return false; + const subs = splitCommands(cmd); + return subs.length > 0 && subs.every((sub) => mine.some((r) => matchBashSpec(r.spec!, sub))); + } + if (PATH_TOOLS.has(tool)) { + const target = targetPath(input, cwd); + return mine.some((r) => matchPathSpec(r.spec!, target)); + } + return false; +} + +/** + * Règles au format Claude Code : "Bash(npm run test:*)", "Edit(src/**)", "Read(./.env)", "Write". + * Priorité : deny > ask > allow ; sans règle applicable → null (permission par défaut de l'outil). + */ +export function createPermissionPolicy(rules: PermissionRules): PermissionPolicy | null { + const deny = parseRules(rules.deny); + const ask = parseRules(rules.ask); + const allow = parseRules(rules.allow); + if (deny.length + ask.length + allow.length === 0) return null; + + return { + check(tool, input, { cwd }) { + const d = deny.find((r) => ruleApplies(r, tool, input, cwd)); + if (d) return { decision: "deny", reason: `règle deny ${d.raw}` }; + const a = ask.find((r) => ruleApplies(r, tool, input, cwd)); + if (a) return { decision: "ask", reason: `règle ask ${a.raw}` }; + if (allowApplies(allow, tool, input, cwd)) return { decision: "allow" }; + return null; + }, + }; +} diff --git a/packages/ext-claudecode/src/report.ts b/packages/ext-claudecode/src/report.ts new file mode 100644 index 0000000..7d840c8 --- /dev/null +++ b/packages/ext-claudecode/src/report.ts @@ -0,0 +1,67 @@ +import { relative, resolve, sep } from "node:path"; +import type { ClaudeConfig } from "./memory"; + +/** Ce qui a été réellement exploité depuis les dossiers .claude. */ +export interface LoadedSummary { + settingsFiles: string[]; + rules: { allow: number; deny: number; ask: number }; + envCount: number; + commands: string[]; + skills: string[]; +} + +/** ~/… pour le dossier personnel, ./… pour ce qui est sous le dossier courant. */ +function displayPath(path: string, cwd: string, homeDir: string): string { + const abs = resolve(path); + const rel = relative(resolve(cwd), abs); + if (rel === "") return "."; + if (!rel.startsWith("..")) return `.${sep}${rel}`; + const fromHome = relative(resolve(homeDir), abs); + if (!fromHome.startsWith("..") && fromHome !== "") return `~${sep}${fromHome}`; + return abs; +} + +/** Texte affiché à l'initialisation, ou null si aucune configuration ClaudeCode n'a été trouvée. */ +export function formatConfigReport( + config: ClaudeConfig, + cwd: string, + homeDir: string, + loaded?: LoadedSummary, +): string | null { + const { memoryFiles, claudeDirs } = config; + if (memoryFiles.length === 0 && claudeDirs.length === 0) return null; + + const lines = [ + "Configuration ClaudeCode trouvée (recherche : ~/.claude, puis chaque dossier de la racine jusqu'au dossier de travail) :", + ]; + lines.push(" Fichiers de contexte (CLAUDE.md) :"); + if (memoryFiles.length === 0) lines.push(" (aucun)"); + for (const f of memoryFiles) lines.push(` • ${displayPath(f.path, cwd, homeDir)}`); + + lines.push(" Dossiers .claude :"); + if (claudeDirs.length === 0) lines.push(" (aucun)"); + for (const d of claudeDirs) { + const used = d.entries.length > 0 ? d.entries.join(", ") : "aucun élément exploité"; + const skipped = d.unsupported.length > 0 ? ` ; non exploité : ${d.unsupported.join(", ")}` : ""; + lines.push(` • ${displayPath(d.path, cwd, homeDir)} (${used}${skipped})`); + } + + if (loaded) { + const items: string[] = []; + if (loaded.settingsFiles.length > 0) { + const { allow, deny, ask } = loaded.rules; + const files = loaded.settingsFiles.map((f) => displayPath(f, cwd, homeDir)).join(", "); + const env = loaded.envCount > 0 ? `, ${loaded.envCount} variable(s) d'environnement` : ""; + items.push(`settings : ${files} (règles : ${allow} allow, ${ask} ask, ${deny} deny${env})`); + } + if (loaded.commands.length > 0) { + items.push(`commandes : ${loaded.commands.map((c) => `/${c}`).join(", ")}`); + } + if (loaded.skills.length > 0) items.push(`skills : ${loaded.skills.join(", ")}`); + if (items.length > 0) { + lines.push(" Chargé depuis .claude :"); + for (const item of items) lines.push(` • ${item}`); + } + } + return lines.join("\n"); +} diff --git a/packages/ext-claudecode/src/settings.ts b/packages/ext-claudecode/src/settings.ts new file mode 100644 index 0000000..ff390fd --- /dev/null +++ b/packages/ext-claudecode/src/settings.ts @@ -0,0 +1,71 @@ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; + +export interface PermissionRules { + allow: string[]; + deny: string[]; + ask: string[]; +} + +export interface ClaudeSettings { + permissions: PermissionRules; + env: Record; + /** Fichiers settings réellement lus. */ + files: string[]; +} + +const SETTINGS_FILES = ["settings.json", "settings.local.json"]; + +function strings(v: unknown): string[] { + return Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : []; +} + +/** + * Fusionne les settings des dossiers .claude, du plus général au plus spécifique + * (settings.json puis settings.local.json dans chaque dossier). Les listes de règles + * s'additionnent ; pour `env`, la dernière valeur l'emporte. + */ +export async function loadSettings( + claudeDirs: string[], +): Promise<{ settings: ClaudeSettings; warnings: string[] }> { + const settings: ClaudeSettings = { + permissions: { allow: [], deny: [], ask: [] }, + env: {}, + files: [], + }; + const warnings: string[] = []; + + for (const dir of claudeDirs) { + for (const name of SETTINGS_FILES) { + const path = join(dir, name); + let raw: string; + try { + raw = await readFile(path, "utf8"); + } catch { + continue; // absent : normal + } + let json: { permissions?: Record; env?: Record }; + try { + json = JSON.parse(raw); + } catch (err) { + warnings.push(`${path} ignoré : JSON invalide (${(err as Error).message})`); + continue; + } + settings.files.push(path); + + for (const key of ["allow", "deny", "ask"] as const) { + settings.permissions[key].push(...strings(json.permissions?.[key])); + } + for (const [k, v] of Object.entries(json.env ?? {})) { + if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") { + settings.env[k] = String(v); + } + } + } + } + + for (const key of ["allow", "deny", "ask"] as const) { + settings.permissions[key] = [...new Set(settings.permissions[key])]; + } + return { settings, warnings }; +} diff --git a/packages/ext-claudecode/src/skills.ts b/packages/ext-claudecode/src/skills.ts new file mode 100644 index 0000000..68f79d0 --- /dev/null +++ b/packages/ext-claudecode/src/skills.ts @@ -0,0 +1,119 @@ +import { readFile, readdir } from "node:fs/promises"; +import { join } from "node:path"; +import type { AgentSkill, Tool } from "@magiccode/core"; +import { renderCommand } from "./commands"; +import { parseFrontmatter } from "./frontmatter"; + +export interface Skill { + name: string; + description: string; + /** Dossier du skill (les fichiers qu'il référence sont relatifs à ce dossier). */ + dir: string; + /** Contenu de SKILL.md sans l'en-tête. */ + body: string; +} + +/** + * Charge .claude/skills//SKILL.md de chaque dossier .claude (du plus général au plus + * spécifique : un skill du projet remplace celui du même nom au niveau utilisateur). + * Seuls le nom et la description sont mis en avant ; le contenu est chargé à la demande (outil Skill). + */ +export async function loadSkills( + claudeDirs: string[], +): Promise<{ skills: Skill[]; warnings: string[] }> { + const byName = new Map(); + const warnings: string[] = []; + + for (const dir of claudeDirs) { + const root = join(dir, "skills"); + let entries; + try { + entries = await readdir(root, { withFileTypes: true }); + } catch { + continue; + } + entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); + + for (const e of entries) { + if (!e.isDirectory()) continue; + const skillDir = join(root, e.name); + let raw: string; + try { + raw = await readFile(join(skillDir, "SKILL.md"), "utf8"); + } catch { + continue; // dossier sans SKILL.md : pas un skill + } + const { data, body } = parseFrontmatter(raw); + const name = data.name || e.name; + let description = data.description; + if (!description) { + description = (body.split("\n").find((l) => l.trim() !== "") ?? "").replace(/^#+\s*/, "").trim(); + warnings.push(`Skill "${name}" sans description dans l'en-tête de SKILL.md (${skillDir})`); + } + byName.set(name, { name, description: description || "(sans description)", dir: skillDir, body }); + } + } + return { skills: [...byName.values()], warnings }; +} + +export function skillsPromptSection(skills: Skill[]): string { + if (skills.length === 0) return ""; + const list = skills.map((s) => `- ${s.name} : ${s.description}`).join("\n"); + return ( + "\n# Skills disponibles\n" + + "Quand une tâche correspond à la description d'un skill, charge-le avec l'outil Skill AVANT de commencer, " + + "puis suis ses instructions.\n" + + list + ); +} + +/** Contenu d'un skill tel que présenté au modèle (outil Skill ou appel direct). */ +function skillContent(skill: Skill): string { + return ( + `# Skill : ${skill.name}\n` + + `Dossier du skill : ${skill.dir} (les fichiers qu'il référence sont relatifs à ce dossier ; lis-les avec Read si besoin)\n\n` + + skill.body.trim() + ); +} + +/** + * Skills appelables directement par l'utilisateur ("§nom [arguments]") : le prompt contient + * les instructions du skill, le modèle les applique sans avoir à appeler l'outil Skill. + */ +export function toAgentSkills(skills: Skill[]): AgentSkill[] { + return skills.map((skill) => ({ + name: skill.name, + description: skill.description, + invoke: (args) => + `L'utilisateur lance explicitement le skill "${skill.name}". Applique maintenant ses instructions.\n\n` + + renderCommand(skillContent(skill), args), + })); +} + +/** Outil Skill : renvoie les instructions complètes du skill demandé (chargement progressif). */ +export function createSkillTool(skills: Skill[]): Tool { + const byName = new Map(skills.map((s) => [s.name, s])); + return { + spec: { + name: "Skill", + description: + "Charge les instructions détaillées d'un skill. Skills disponibles :\n" + + skills.map((s) => `- ${s.name} : ${s.description}`).join("\n"), + inputSchema: { + type: "object", + properties: { skill: { type: "string", enum: skills.map((s) => s.name) } }, + required: ["skill"], + }, + }, + permission: "auto", + describe: (i) => String(i.skill ?? ""), + async execute(input) { + const name = typeof input.skill === "string" ? input.skill : ""; + const skill = byName.get(name); + if (!skill) { + throw new Error(`Skill inconnu : "${name}" (disponibles : ${[...byName.keys()].join(", ")})`); + } + return skillContent(skill); + }, + }; +} diff --git a/packages/ext-claudecode/src/tools/bash.ts b/packages/ext-claudecode/src/tools/bash.ts new file mode 100644 index 0000000..13a3843 --- /dev/null +++ b/packages/ext-claudecode/src/tools/bash.ts @@ -0,0 +1,40 @@ +import { exec } from "node:child_process"; +import type { Tool } from "@magiccode/core"; +import { str } from "./helpers"; + +const OUTPUT_MAX = 30_000; + +export const bashTool: Tool = { + spec: { + name: "Bash", + description: "Exécute une commande shell dans le dossier de lancement de MagicCode (timeout : 2 minutes).", + inputSchema: { + type: "object", + properties: { command: { type: "string" } }, + required: ["command"], + }, + }, + permission: "ask", + describe: (i) => String(i.command ?? ""), + execute(input, ctx) { + const command = str(input, "command"); + return new Promise((resolve) => { + exec( + command, + { + cwd: ctx.cwd, + timeout: 120_000, + maxBuffer: 10 * 1024 * 1024, + signal: ctx.signal, + env: { ...process.env, ...ctx.env }, + }, + (error, stdout, stderr) => { + let out = `${stdout}${stderr ? `\n[stderr]\n${stderr}` : ""}`.trim(); + if (out.length > OUTPUT_MAX) out = `${out.slice(0, OUTPUT_MAX)}\n… (tronqué)`; + const code = error ? ((error as { code?: number }).code ?? 1) : 0; + resolve(`${out}\n[code de sortie : ${code}]`.trim()); + }, + ); + }); + }, +}; diff --git a/packages/ext-claudecode/src/tools/edit.ts b/packages/ext-claudecode/src/tools/edit.ts new file mode 100644 index 0000000..8e623da --- /dev/null +++ b/packages/ext-claudecode/src/tools/edit.ts @@ -0,0 +1,36 @@ +import { readFile, writeFile } from "node:fs/promises"; +import type { Tool } from "@magiccode/core"; +import { resolvePath, str } from "./helpers"; + +export const editTool: Tool = { + spec: { + name: "Edit", + description: + "Remplace une occurrence unique de old_string par new_string dans un fichier. " + + "Échoue si old_string est absent ou présent plusieurs fois.", + inputSchema: { + type: "object", + properties: { + path: { type: "string", description: "Chemin du fichier : complet, ou relatif au dossier de lancement de MagicCode" }, + old_string: { type: "string" }, + new_string: { type: "string" }, + }, + required: ["path", "old_string", "new_string"], + }, + }, + permission: "ask", + describe: (i) => String(i.path ?? ""), + async execute(input, ctx) { + const file = resolvePath(ctx.cwd, str(input, "path")); + const oldStr = str(input, "old_string"); + const newStr = str(input, "new_string"); + const text = await readFile(file, "utf8"); + + const count = text.split(oldStr).length - 1; + if (count === 0) throw new Error("old_string introuvable dans le fichier"); + if (count > 1) throw new Error(`old_string présent ${count} fois : ajoutez du contexte`); + + await writeFile(file, text.replace(oldStr, () => newStr), "utf8"); + return `Fichier modifié : ${file}`; + }, +}; diff --git a/packages/ext-claudecode/src/tools/glob.ts b/packages/ext-claudecode/src/tools/glob.ts new file mode 100644 index 0000000..e2590ef --- /dev/null +++ b/packages/ext-claudecode/src/tools/glob.ts @@ -0,0 +1,62 @@ +import { stat } from "node:fs/promises"; +import { relative, sep } from "node:path"; +import picomatch from "picomatch"; +import type { Tool } from "@magiccode/core"; +import { displayPath, optStr, resolvePath, str } from "./helpers"; +import { walkFiles } from "./walk"; + +const MAX_RESULTS = 100; + +export const globTool: Tool = { + spec: { + name: "Glob", + description: + 'Trouve des fichiers par motif glob (ex. "**/*.ts", "src/**/*.test.ts", "*.json"). ' + + "Le motif est relatif au dossier de recherche. Résultats triés par date de modification " + + "(les plus récents d'abord), limités à 100. Ignore .git, node_modules et les fichiers exclus par .gitignore.", + inputSchema: { + type: "object", + properties: { + pattern: { type: "string", description: "Motif glob à rechercher" }, + path: { + type: "string", + description: "Dossier de recherche : complet ou relatif au dossier de lancement (défaut : ce dossier).", + }, + }, + required: ["pattern"], + }, + }, + permission: "auto", + describe: (i) => `${String(i.pattern ?? "")}${i.path ? ` dans ${String(i.path)}` : ""}`, + async execute(input, ctx) { + const pattern = str(input, "pattern"); + const root = resolvePath(ctx.cwd, optStr(input, "path") ?? "."); + + try { + if (!(await stat(root)).isDirectory()) throw new Error(); + } catch { + throw new Error(`Dossier de recherche introuvable : ${root}`); + } + + const isMatch = picomatch(pattern, { dot: true }); + const matched: string[] = []; + for await (const abs of walkFiles(root, ctx.signal)) { + if (isMatch(relative(root, abs).split(sep).join("/"))) matched.push(abs); + } + if (matched.length === 0) return `Aucun fichier ne correspond au motif "${pattern}"`; + + const withTime = await Promise.all( + matched.map(async (abs) => ({ + abs, + mtime: await stat(abs).then((s) => s.mtimeMs, () => 0), + })), + ); + withTime.sort((a, b) => b.mtime - a.mtime); + + const shown = withTime.slice(0, MAX_RESULTS).map((f) => displayPath(ctx.cwd, f.abs)); + const extra = withTime.length - shown.length; + return extra > 0 + ? `${shown.join("\n")}\n(${extra} résultat(s) supplémentaire(s) non affiché(s) : affinez le motif)` + : shown.join("\n"); + }, +}; diff --git a/packages/ext-claudecode/src/tools/grep.ts b/packages/ext-claudecode/src/tools/grep.ts new file mode 100644 index 0000000..4891a96 --- /dev/null +++ b/packages/ext-claudecode/src/tools/grep.ts @@ -0,0 +1,161 @@ +import { readFile, stat } from "node:fs/promises"; +import { relative, sep } from "node:path"; +import picomatch from "picomatch"; +import type { Tool } from "@magiccode/core"; +import { displayPath, optBool, optNum, optStr, resolvePath, str } from "./helpers"; +import { walkFiles } from "./walk"; + +const MAX_FILE_BYTES = 2 * 1024 * 1024; +const MAX_LINE_CHARS = 500; +const DEFAULT_HEAD_LIMIT = 250; + +type Mode = "files_with_matches" | "content" | "count"; +const MODES: Mode[] = ["files_with_matches", "content", "count"]; + +/** Retourne le texte, ou null pour un fichier trop gros, binaire ou illisible. */ +async function readTextFile(abs: string): Promise { + try { + if ((await stat(abs)).size > MAX_FILE_BYTES) return null; + const buf = await readFile(abs); + if (buf.subarray(0, 8000).includes(0)) return null; + return buf.toString("utf8"); + } catch { + return null; + } +} + +function splitLines(text: string): string[] { + const lines = text.split("\n"); + if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop(); + return lines.map((l) => (l.endsWith("\r") ? l.slice(0, -1) : l)); +} + +const clip = (l: string): string => (l.length > MAX_LINE_CHARS ? `${l.slice(0, MAX_LINE_CHARS)}…` : l); + +export const grepTool: Tool = { + spec: { + name: "Grep", + description: + "Recherche une expression régulière (syntaxe JavaScript) dans le contenu des fichiers. " + + "Ignore .git, node_modules, les fichiers exclus par .gitignore et les fichiers binaires. " + + 'output_mode : "files_with_matches" (défaut, liste des fichiers), "content" (lignes correspondantes) ou "count".', + inputSchema: { + type: "object", + properties: { + pattern: { type: "string", description: "Expression régulière à rechercher" }, + path: { type: "string", description: "Fichier ou dossier : chemin complet ou relatif au dossier de lancement (défaut : ce dossier)" }, + glob: { type: "string", description: 'Filtre sur les fichiers, ex. "*.ts" ou "src/**/*.tsx"' }, + output_mode: { type: "string", enum: MODES }, + "-i": { type: "boolean", description: "Ignorer la casse" }, + "-n": { type: "boolean", description: "Numéros de ligne (mode content, vrai par défaut)" }, + "-A": { type: "number", description: "Lignes de contexte après chaque résultat (mode content)" }, + "-B": { type: "number", description: "Lignes de contexte avant chaque résultat (mode content)" }, + "-C": { type: "number", description: "Lignes de contexte avant et après (mode content)" }, + head_limit: { + type: "number", + description: `Nombre maximum de lignes/entrées renvoyées (défaut ${DEFAULT_HEAD_LIMIT}, 0 = sans limite)`, + }, + }, + required: ["pattern"], + }, + }, + permission: "auto", + describe: (i) => `${String(i.pattern ?? "")}${i.path ? ` dans ${String(i.path)}` : ""}`, + async execute(input, ctx) { + const pattern = str(input, "pattern"); + + const mode = (optStr(input, "output_mode") ?? "files_with_matches") as Mode; + if (!MODES.includes(mode)) { + throw new Error(`output_mode invalide : "${mode}" (attendu : ${MODES.join(", ")})`); + } + + let regex: RegExp; + try { + regex = new RegExp(pattern, optBool(input, "-i") ? "i" : ""); + } catch (err) { + throw new Error(`Expression régulière invalide : ${(err as Error).message}`); + } + + const showLineNumbers = optBool(input, "-n") ?? true; + const both = optNum(input, "-C"); + const after = Math.max(optNum(input, "-A") ?? both ?? 0, 0); + const before = Math.max(optNum(input, "-B") ?? both ?? 0, 0); + const headLimit = optNum(input, "head_limit"); + const limit = headLimit === undefined ? DEFAULT_HEAD_LIMIT : headLimit <= 0 ? Infinity : headLimit; + + const target = resolvePath(ctx.cwd, optStr(input, "path") ?? "."); + let targetIsFile: boolean; + try { + targetIsFile = (await stat(target)).isFile(); + } catch { + throw new Error(`Chemin introuvable : ${target}`); + } + + const globFilter = optStr(input, "glob"); + const matchGlob = globFilter ? picomatch(globFilter, { dot: true, basename: true }) : null; + + async function* candidates(): AsyncGenerator { + if (targetIsFile) { + yield target; + return; + } + for await (const abs of walkFiles(target, ctx.signal)) { + if (matchGlob && !matchGlob(relative(target, abs).split(sep).join("/"))) continue; + yield abs; + } + } + + const out: string[] = []; + let truncated = false; + + for await (const abs of candidates()) { + if (ctx.signal?.aborted) break; + const text = await readTextFile(abs); + if (text === null) continue; + + const lines = splitLines(text); + const hits: number[] = []; + for (let i = 0; i < lines.length; i++) { + if (regex.test(lines[i] ?? "")) { + hits.push(i); + if (mode === "files_with_matches") break; + } + } + if (hits.length === 0) continue; + + const name = displayPath(ctx.cwd, abs); + if (mode === "files_with_matches") { + out.push(name); + } else if (mode === "count") { + out.push(`${name}:${hits.length}`); + } else { + // index de ligne → true si c'est une ligne correspondante (sinon : ligne de contexte) + const show = new Map(); + for (const m of hits) { + const from = Math.max(0, m - before); + const to = Math.min(lines.length - 1, m + after); + for (let j = from; j <= to; j++) show.set(j, (show.get(j) ?? false) || j === m); + } + let prev = -2; + for (const j of [...show.keys()].sort((a, b) => a - b)) { + if (prev >= 0 && j > prev + 1) out.push("--"); + const sepChar = show.get(j) ? ":" : "-"; + const num = showLineNumbers ? `${j + 1}${sepChar}` : ""; + out.push(`${name}${sepChar}${num}${clip(lines[j] ?? "")}`); + prev = j; + } + } + + if (out.length >= limit) { + out.length = Math.min(out.length, limit); + truncated = true; + break; + } + } + + if (out.length === 0) return `Aucun résultat pour "${pattern}"`; + return truncated + ? `${out.join("\n")}\n(limite de ${limit} résultats atteinte : affinez la recherche ou augmentez head_limit)` + : out.join("\n"); + }, +}; diff --git a/packages/ext-claudecode/src/tools/helpers.ts b/packages/ext-claudecode/src/tools/helpers.ts new file mode 100644 index 0000000..47f8df8 --- /dev/null +++ b/packages/ext-claudecode/src/tools/helpers.ts @@ -0,0 +1,40 @@ +import { homedir } from "node:os"; +import { isAbsolute, join, relative, resolve, sep } from "node:path"; + +export function str(input: Record, key: string): string { + const v = input[key]; + if (typeof v !== "string") throw new Error(`Paramètre "${key}" manquant ou invalide`); + return v; +} + +export function optStr(input: Record, key: string): string | undefined { + const v = input[key]; + return typeof v === "string" && v !== "" ? v : undefined; +} + +export function optNum(input: Record, key: string): number | undefined { + const v = input[key]; + return typeof v === "number" ? v : undefined; +} + +export function optBool(input: Record, key: string): boolean | undefined { + const v = input[key]; + return typeof v === "boolean" ? v : undefined; +} + +/** + * Chemin relatif → résolu depuis `cwd` (dossier de lancement de MagicCode) ; + * chemin complet → utilisé tel quel ; "~" et "~/…" → dossier personnel. + */ +export function resolvePath(cwd: string, p: string): string { + if (p === "~") return homedir(); + if (p.startsWith("~/") || p.startsWith("~\\")) return join(homedir(), p.slice(2)); + return resolve(cwd, p); +} + +/** Chemin affiché : relatif au dossier courant (séparateur "/"), absolu s'il est en dehors. */ +export function displayPath(cwd: string, abs: string): string { + const rel = relative(cwd, abs); + const out = rel === "" ? "." : rel.startsWith("..") || isAbsolute(rel) ? abs : rel; + return out.split(sep).join("/"); +} diff --git a/packages/ext-claudecode/src/tools/index.ts b/packages/ext-claudecode/src/tools/index.ts new file mode 100644 index 0000000..c8d380f --- /dev/null +++ b/packages/ext-claudecode/src/tools/index.ts @@ -0,0 +1,10 @@ +import type { Tool } from "@magiccode/core"; +import { bashTool } from "./bash"; +import { editTool } from "./edit"; +import { globTool } from "./glob"; +import { grepTool } from "./grep"; +import { readTool } from "./read"; +import { writeTool } from "./write"; + +// Lot 1 complet. TODO : TodoWrite, WebFetch, Task (lot 3). +export const claudeCodeTools: Tool[] = [readTool, globTool, grepTool, writeTool, editTool, bashTool]; diff --git a/packages/ext-claudecode/src/tools/paths.test.ts b/packages/ext-claudecode/src/tools/paths.test.ts new file mode 100644 index 0000000..01d0073 --- /dev/null +++ b/packages/ext-claudecode/src/tools/paths.test.ts @@ -0,0 +1,37 @@ +import { mkdtemp, readFile } from "node:fs/promises"; +import { homedir, tmpdir } from "node:os"; +import { basename, join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { bashTool } from "./bash"; +import { resolvePath } from "./helpers"; +import { readTool } from "./read"; +import { writeTool } from "./write"; + +describe("chemins relatifs au dossier de lancement", () => { + it("resolvePath : relatif → cwd, complet → inchangé, ~ → dossier personnel", () => { + expect(resolvePath("/launch/X", "a/b.txt")).toBe(join("/launch/X", "a/b.txt")); + expect(resolvePath("/launch/X", "/etc/hosts")).toBe("/etc/hosts"); + expect(resolvePath("/launch/X", "~/f.txt")).toBe(join(homedir(), "f.txt")); + }); + + it("Write crée le fichier depuis le dossier de lancement, Read le relit depuis là", async () => { + const cwd = await mkdtemp(join(tmpdir(), "magiccode-launch-")); + await writeTool.execute({ path: "sub/new.txt", content: "bonjour" }, { cwd }); + expect(await readFile(join(cwd, "sub", "new.txt"), "utf8")).toBe("bonjour"); + expect(await readTool.execute({ path: "sub/new.txt" }, { cwd })).toContain("bonjour"); + }); + + it("Write avec chemin complet écrit exactement là", async () => { + const cwd = await mkdtemp(join(tmpdir(), "magiccode-launch-")); + const elsewhere = await mkdtemp(join(tmpdir(), "magiccode-other-")); + const target = join(elsewhere, "abs.txt"); + await writeTool.execute({ path: target, content: "x" }, { cwd }); + expect(await readFile(target, "utf8")).toBe("x"); + }); + + it("Bash s'exécute dans le dossier de lancement", async () => { + const cwd = await mkdtemp(join(tmpdir(), "magiccode-launch-")); + const out = await bashTool.execute({ command: "pwd" }, { cwd }); + expect(out).toContain(basename(cwd)); + }); +}); diff --git a/packages/ext-claudecode/src/tools/read.ts b/packages/ext-claudecode/src/tools/read.ts new file mode 100644 index 0000000..ca63023 --- /dev/null +++ b/packages/ext-claudecode/src/tools/read.ts @@ -0,0 +1,31 @@ +import { readFile } from "node:fs/promises"; +import type { Tool } from "@magiccode/core"; +import { optNum, resolvePath, str } from "./helpers"; + +export const readTool: Tool = { + spec: { + name: "Read", + description: "Lit un fichier texte et renvoie son contenu avec les numéros de ligne.", + inputSchema: { + type: "object", + properties: { + path: { type: "string", description: "Chemin du fichier : complet, ou relatif au dossier de lancement de MagicCode" }, + offset: { type: "number", description: "Première ligne à lire (1 par défaut)" }, + limit: { type: "number", description: "Nombre maximum de lignes (2000 par défaut)" }, + }, + required: ["path"], + }, + }, + permission: "auto", + describe: (i) => String(i.path ?? ""), + async execute(input, ctx) { + const file = resolvePath(ctx.cwd, str(input, "path")); + const lines = (await readFile(file, "utf8")).split("\n"); + const start = Math.max((optNum(input, "offset") ?? 1) - 1, 0); + const end = start + (optNum(input, "limit") ?? 2000); + return lines + .slice(start, end) + .map((l, i) => `${String(start + i + 1).padStart(6)}\t${l}`) + .join("\n"); + }, +}; diff --git a/packages/ext-claudecode/src/tools/search-tools.test.ts b/packages/ext-claudecode/src/tools/search-tools.test.ts new file mode 100644 index 0000000..602d09c --- /dev/null +++ b/packages/ext-claudecode/src/tools/search-tools.test.ts @@ -0,0 +1,73 @@ +import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { beforeAll, describe, expect, it } from "vitest"; +import { globTool } from "./glob"; +import { grepTool } from "./grep"; + +let cwd: string; +const ctx = () => ({ cwd }); + +beforeAll(async () => { + cwd = await mkdtemp(join(tmpdir(), "magiccode-search-")); + await mkdir(join(cwd, "src"), { recursive: true }); + await mkdir(join(cwd, "node_modules", "lib"), { recursive: true }); + await writeFile(join(cwd, "a.ts"), "const alpha = 1;\n"); + await writeFile(join(cwd, "src", "b.ts"), "// début\nconst hello = 1;\nconst Hello2 = 2;\n// fin\n"); + await writeFile(join(cwd, "src", "c.md"), "# hello doc\n"); + await writeFile(join(cwd, "node_modules", "lib", "x.ts"), "const hello = 0;\n"); + await writeFile(join(cwd, "debug.log"), "hello log\n"); + await writeFile(join(cwd, ".gitignore"), "*.log\n"); + await writeFile(join(cwd, "bin.dat"), Buffer.from([104, 101, 108, 108, 111, 0, 1, 2])); +}); + +describe("Glob", () => { + it("trouve les fichiers et ignore node_modules et .gitignore", async () => { + const out = await globTool.execute({ pattern: "**/*.ts" }, ctx()); + expect(out.split("\n").sort()).toEqual(["a.ts", "src/b.ts"]); + }); + + it("*.ts ne descend pas dans les sous-dossiers", async () => { + expect(await globTool.execute({ pattern: "*.ts" }, ctx())).toBe("a.ts"); + }); + + it("indique l'absence de résultat", async () => { + expect(await globTool.execute({ pattern: "**/*.rs" }, ctx())).toContain("Aucun fichier"); + }); +}); + +describe("Grep", () => { + it("liste les fichiers correspondants (défaut)", async () => { + const out = await grepTool.execute({ pattern: "hello" }, ctx()); + expect(out.split("\n").sort()).toEqual(["src/b.ts", "src/c.md"]); // ni node_modules, ni .log, ni binaire + }); + + it("mode content avec numéros de ligne et filtre glob", async () => { + const out = await grepTool.execute( + { pattern: "hello", output_mode: "content", glob: "*.ts", "-i": true }, + ctx(), + ); + expect(out.split("\n")).toEqual(["src/b.ts:2:const hello = 1;", "src/b.ts:3:const Hello2 = 2;"]); + }); + + it("mode content avec contexte", async () => { + const out = await grepTool.execute( + { pattern: "hello", output_mode: "content", path: "src/b.ts", "-C": 1 }, + ctx(), + ); + expect(out.split("\n")).toEqual([ + "src/b.ts-1-// début", + "src/b.ts:2:const hello = 1;", + "src/b.ts-3-const Hello2 = 2;", + ]); + }); + + it("mode count", async () => { + const out = await grepTool.execute({ pattern: "const", output_mode: "count" }, ctx()); + expect(out.split("\n").sort()).toEqual(["a.ts:1", "src/b.ts:2"]); + }); + + it("signale une regex invalide", async () => { + await expect(grepTool.execute({ pattern: "(" }, ctx())).rejects.toThrow("Expression régulière invalide"); + }); +}); diff --git a/packages/ext-claudecode/src/tools/walk.ts b/packages/ext-claudecode/src/tools/walk.ts new file mode 100644 index 0000000..a6b7983 --- /dev/null +++ b/packages/ext-claudecode/src/tools/walk.ts @@ -0,0 +1,65 @@ +import { readFile, readdir } from "node:fs/promises"; +import { join, relative, sep } from "node:path"; +import ignore, { type Ignore } from "ignore"; + +/** Toujours ignorés, même sans .gitignore. */ +const ALWAYS_SKIPPED = new Set([".git", "node_modules"]); + +interface Scope { + base: string; + ig: Ignore; +} + +async function loadGitignore(dir: string): Promise { + try { + const text = await readFile(join(dir, ".gitignore"), "utf8"); + return { base: dir, ig: ignore().add(text) }; + } catch { + return null; + } +} + +function isIgnored(abs: string, isDir: boolean, scopes: Scope[]): boolean { + for (const s of scopes) { + const rel = relative(s.base, abs).split(sep).join("/"); + if (!rel || rel.startsWith("..")) continue; + if (s.ig.ignores(isDir ? `${rel}/` : rel)) return true; + } + return false; +} + +/** + * Parcourt récursivement `root` (ordre alphabétique) et produit les chemins absolus des fichiers. + * Ignore .git, node_modules et ce qu'excluent les .gitignore rencontrés (racine et sous-dossiers). + * Les liens symboliques ne sont pas suivis. + */ +export async function* walkFiles(root: string, signal?: AbortSignal): AsyncGenerator { + yield* walk(root, [], signal); +} + +async function* walk(dir: string, inherited: Scope[], signal?: AbortSignal): AsyncGenerator { + if (signal?.aborted) return; + + const local = await loadGitignore(dir); + const scopes = local ? [...inherited, local] : inherited; + + let entries; + try { + entries = await readdir(dir, { withFileTypes: true }); + } catch { + return; // dossier illisible : ignoré + } + entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); + + for (const e of entries) { + if (ALWAYS_SKIPPED.has(e.name)) continue; + const abs = join(dir, e.name); + if (e.isDirectory()) { + if (isIgnored(abs, true, scopes)) continue; + yield* walk(abs, scopes, signal); + } else if (e.isFile()) { + if (isIgnored(abs, false, scopes)) continue; + yield abs; + } + } +} diff --git a/packages/ext-claudecode/src/tools/write.ts b/packages/ext-claudecode/src/tools/write.ts new file mode 100644 index 0000000..4b195f7 --- /dev/null +++ b/packages/ext-claudecode/src/tools/write.ts @@ -0,0 +1,28 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname } from "node:path"; +import type { Tool } from "@magiccode/core"; +import { resolvePath, str } from "./helpers"; + +export const writeTool: Tool = { + spec: { + name: "Write", + description: "Écrit un fichier (le crée ou écrase son contenu).", + inputSchema: { + type: "object", + properties: { + path: { type: "string", description: "Chemin du fichier : complet, ou relatif au dossier de lancement de MagicCode" }, + content: { type: "string" }, + }, + required: ["path", "content"], + }, + }, + permission: "ask", + describe: (i) => String(i.path ?? ""), + async execute(input, ctx) { + const file = resolvePath(ctx.cwd, str(input, "path")); + const content = str(input, "content"); + await mkdir(dirname(file), { recursive: true }); + await writeFile(file, content, "utf8"); + return `Fichier écrit : ${file} (${content.length} caractères)`; + }, +}; diff --git a/packages/ext-claudecode/tsconfig.json b/packages/ext-claudecode/tsconfig.json new file mode 100644 index 0000000..564a599 --- /dev/null +++ b/packages/ext-claudecode/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"] +} diff --git a/packages/providers/package.json b/packages/providers/package.json new file mode 100644 index 0000000..53dc3d3 --- /dev/null +++ b/packages/providers/package.json @@ -0,0 +1,12 @@ +{ + "name": "@magiccode/providers", + "version": "0.1.0", + "type": "module", + "main": "src/index.ts", + "types": "src/index.ts", + "scripts": { "typecheck": "tsc -p ." }, + "dependencies": { + "@anthropic-ai/sdk": "^0.40.0", + "@magiccode/core": "workspace:*" + } +} diff --git a/packages/providers/src/anthropic.ts b/packages/providers/src/anthropic.ts new file mode 100644 index 0000000..4d34667 --- /dev/null +++ b/packages/providers/src/anthropic.ts @@ -0,0 +1,73 @@ +import Anthropic from "@anthropic-ai/sdk"; +import type { ChatRequest, LLMProvider, Message, StopReason, StreamEvent } from "@magiccode/core"; + +function toAnthropicMessages(messages: Message[]): Anthropic.MessageParam[] { + return messages.map((m) => ({ + role: m.role, + content: m.content.map((b) => { + switch (b.type) { + case "text": + return { type: "text" as const, text: b.text }; + case "tool_use": + return { type: "tool_use" as const, id: b.id, name: b.name, input: b.input }; + case "tool_result": + return { + type: "tool_result" as const, + tool_use_id: b.toolUseId, + content: b.content, + is_error: b.isError ?? false, + }; + } + }), + })); +} + +function toStopReason(r: string | null): StopReason { + if (r === "end_turn" || r === "tool_use" || r === "max_tokens") return r; + return "other"; +} + +/** Utilise ANTHROPIC_API_KEY depuis l'environnement. */ +export function createAnthropicProvider(model: string): LLMProvider { + const client = new Anthropic(); + return { + id: "anthropic", + model, + capabilities: { tools: true }, + async *chat(req: ChatRequest, signal?: AbortSignal): AsyncIterable { + const stream = client.messages.stream( + { + model, + max_tokens: req.maxTokens ?? 8192, + system: req.system, + tools: req.tools.map((t) => ({ + name: t.name, + description: t.description, + input_schema: t.inputSchema as Anthropic.Tool.InputSchema, + })), + messages: toAnthropicMessages(req.messages), + }, + { signal }, + ); + + for await (const ev of stream) { + if (ev.type === "content_block_delta" && ev.delta.type === "text_delta") { + yield { type: "text_delta", text: ev.delta.text }; + } + } + + const final = await stream.finalMessage(); + for (const block of final.content) { + if (block.type === "tool_use") { + yield { + type: "tool_use", + id: block.id, + name: block.name, + input: block.input as Record, + }; + } + } + yield { type: "done", stopReason: toStopReason(final.stop_reason) }; + }, + }; +} diff --git a/packages/providers/src/index.ts b/packages/providers/src/index.ts new file mode 100644 index 0000000..30804a6 --- /dev/null +++ b/packages/providers/src/index.ts @@ -0,0 +1,114 @@ +import type { LLMProvider } from "@magiccode/core"; +import { createAnthropicProvider } from "./anthropic"; +import { createOpenAIProvider } from "./openai"; +import { createOllamaProvider } from "./ollama"; +import { createSapAiCoreProvider, listSapDeployments, type SapAiCoreOptions } from "./sapaicore"; + +export { createSapAiCoreProvider, listSapDeployments } from "./sapaicore"; + +/** Options communes : `baseDir` = dossier de lancement de MagicCode (base des chemins relatifs). */ +export interface ProviderOptions { + baseDir?: string; +} + +/** Alias courts → identifiants réels. Pour "sapaicore", le nom du déploiement est utilisé tel quel. */ +export const MODEL_ALIASES: Record> = { + anthropic: { + sonnet: "claude-sonnet-5-5", + opus: "claude-opus-5-5", + haiku: "claude-haiku-5-5", + }, + openai: { gpt: "gpt-4.1" }, // TODO : vérifier l'identifiant à utiliser + ollama: { default: "llama3.1" }, +}; + +export interface ModelChoice { + value: string; // "provider:modèle" + label: string; + hint?: string; +} + +/** Modèles réellement déployés sur le service SAP AI Core (appel réseau). */ +export async function listSapModelChoices(opts?: SapAiCoreOptions): Promise { + const deployments = await listSapDeployments(opts); + return deployments + .filter((d) => d.type !== "other") + .map((d) => ({ + value: `sapaicore:${d.name}`, + label: `${d.name}${d.version ? ` (${d.version})` : ""}`, + hint: "SAP AI Core", + })); +} + +/** API Anthropic directe : proposée seulement si ANTHROPIC_API_KEY est définie. */ +async function listAnthropicModelChoices(): Promise { + if (!process.env.ANTHROPIC_API_KEY) return []; + return [ + { value: "anthropic:sonnet", label: "Claude Sonnet", hint: "API Anthropic" }, + { value: "anthropic:opus", label: "Claude Opus", hint: "API Anthropic" }, + { value: "anthropic:haiku", label: "Claude Haiku", hint: "API Anthropic" }, + ]; +} + +/** + * Chaque provider prêt à l'emploi expose ses modèles disponibles ici. + * (openai et ollama seront ajoutés quand leurs adaptateurs seront implémentés.) + */ +const PROVIDER_CATALOGS: { name: string; load(opts?: ProviderOptions): Promise }[] = [ + { name: "SAP AI Core", load: (opts) => listSapModelChoices({ baseDir: opts?.baseDir }) }, + { name: "Anthropic", load: listAnthropicModelChoices }, +]; + +export interface ModelCatalog { + choices: ModelChoice[]; + /** Providers dont le chargement a échoué (non bloquant). */ + errors: string[]; +} + +/** Interroge tous les providers en parallèle et agrège les modèles disponibles. */ +export async function loadModelChoices(opts?: ProviderOptions): Promise { + const results = await Promise.allSettled(PROVIDER_CATALOGS.map((c) => c.load(opts))); + const choices: ModelChoice[] = []; + const errors: string[] = []; + results.forEach((r, i) => { + const name = PROVIDER_CATALOGS[i]!.name; + if (r.status === "fulfilled") choices.push(...r.value); + else errors.push(`${name} : ${(r.reason as Error).message}`); + }); + return { choices, errors }; +} + +/** + * "sapaicore" (premier modèle disponible), "sapaicore:anthropic--claude-4.5-sonnet", + * "anthropic:sonnet", "ollama:mistral", ou "sonnet" (provider déduit de l'alias). + */ +export function parseModelSpec(spec: string): { provider: string; model: string } { + if (spec === "sapaicore") return { provider: "sapaicore", model: "" }; + const idx = spec.indexOf(":"); + if (idx > 0) { + const provider = spec.slice(0, idx); + const name = spec.slice(idx + 1); + return { provider, model: MODEL_ALIASES[provider]?.[name] ?? name }; + } + for (const [provider, aliases] of Object.entries(MODEL_ALIASES)) { + const model = aliases[spec]; + if (model) return { provider, model }; + } + throw new Error(`Modèle inconnu : "${spec}" (format attendu : provider:modèle)`); +} + +export function createProvider(spec: string, opts?: ProviderOptions): LLMProvider { + const { provider, model } = parseModelSpec(spec); + switch (provider) { + case "sapaicore": + return createSapAiCoreProvider(model, { baseDir: opts?.baseDir }); + case "anthropic": + return createAnthropicProvider(model); + case "openai": + return createOpenAIProvider(model); + case "ollama": + return createOllamaProvider(model); + default: + throw new Error(`Provider inconnu : "${provider}"`); + } +} diff --git a/packages/providers/src/ollama.ts b/packages/providers/src/ollama.ts new file mode 100644 index 0000000..d07e85c --- /dev/null +++ b/packages/providers/src/ollama.ts @@ -0,0 +1,8 @@ +import type { LLMProvider } from "@magiccode/core"; + +// TODO : implémenter via l'API HTTP locale (http://localhost:11434/api/chat, stream NDJSON). +// Prévoir un mode dégradé : tous les modèles Ollama ne supportent pas les outils +// (capabilities.tools = false → avertir l'utilisateur). +export function createOllamaProvider(_model: string): LLMProvider { + throw new Error("Provider Ollama pas encore implémenté"); +} diff --git a/packages/providers/src/openai.ts b/packages/providers/src/openai.ts new file mode 100644 index 0000000..b3b1df9 --- /dev/null +++ b/packages/providers/src/openai.ts @@ -0,0 +1,8 @@ +import type { LLMProvider } from "@magiccode/core"; + +// TODO : implémenter avec le SDK `openai` (chat completions en streaming). +// Il faut convertir : messages neutres → format OpenAI, tool_use ↔ tool_calls, +// et normaliser le streaming en StreamEvent. +export function createOpenAIProvider(_model: string): LLMProvider { + throw new Error("Provider OpenAI pas encore implémenté"); +} diff --git a/packages/providers/src/sapaicore/auth.ts b/packages/providers/src/sapaicore/auth.ts new file mode 100644 index 0000000..830f1e9 --- /dev/null +++ b/packages/providers/src/sapaicore/auth.ts @@ -0,0 +1,68 @@ +import type { BtpAiConfig } from "./config"; +import { safeReadText } from "./http"; + +/** + * Récupère un token de session (OAuth client_credentials) auprès du service. + * + * GET {config.url}/oauth/token?grant_type=client_credentials + * Header: Authorization: Basic {config.basic} + * Réponse attendue: { access_token: "...", expires_in?: number, ... } + */ +export async function fetchToken( + config: BtpAiConfig, +): Promise<{ accessToken: string; expiresIn?: number }> { + const tokenUrl = new URL(`${config.url.replace(/\/+$/, "")}/oauth/token`); + tokenUrl.searchParams.set("grant_type", "client_credentials"); + + const response = await fetch(tokenUrl, { + method: "GET", + headers: { Authorization: `Basic ${config.basic}` }, + }); + + if (!response.ok) { + const body = await safeReadText(response); + throw new Error( + `Échec de récupération du token de session (HTTP ${response.status} ${response.statusText}). ${body}`, + ); + } + + const data = (await response.json()) as { access_token?: string; expires_in?: number }; + if (!data.access_token) { + throw new Error( + "La réponse du service d'authentification ne contient pas de champ 'access_token'.", + ); + } + return { accessToken: data.access_token, expiresIn: data.expires_in }; +} + +export async function fetchSessionToken(config: BtpAiConfig): Promise { + return (await fetchToken(config)).accessToken; +} + +/** Token mis en cache jusqu'à son expiration (marge de 60 s), renouvelé à la demande. */ +export function createTokenProvider(config: BtpAiConfig) { + let cached: { token: string; expiresAt: number } | undefined; + let pending: Promise | undefined; + + const refresh = async (): Promise => { + const { accessToken, expiresIn } = await fetchToken(config); + const ttl = Math.max((expiresIn ?? 300) - 60, 30); + cached = { token: accessToken, expiresAt: Date.now() + ttl * 1000 }; + return accessToken; + }; + + return { + async get(): Promise { + if (cached && Date.now() < cached.expiresAt) return cached.token; + pending ??= refresh().finally(() => { + pending = undefined; + }); + return pending; + }, + invalidate(): void { + cached = undefined; + }, + }; +} + +export type TokenProvider = ReturnType; diff --git a/packages/providers/src/sapaicore/claude.ts b/packages/providers/src/sapaicore/claude.ts new file mode 100644 index 0000000..c37f3cb --- /dev/null +++ b/packages/providers/src/sapaicore/claude.ts @@ -0,0 +1,87 @@ +import type { ChatRequest, StopReason, StreamEvent } from "@magiccode/core"; + +// Claude sur AI Core = déploiement AWS Bedrock → API Converse : POST {deploymentUrl}/converse + +type ConverseBlock = + | { text: string } + | { toolUse: { toolUseId: string; name: string; input: Record } } + | { + toolResult: { + toolUseId: string; + content: { text: string }[]; + status: "success" | "error"; + }; + }; + +export function buildConverseBody(req: ChatRequest): Record { + const messages = req.messages.map((m) => ({ + role: m.role, + content: m.content.flatMap((b): ConverseBlock[] => { + switch (b.type) { + case "text": + return b.text ? [{ text: b.text }] : []; // Bedrock refuse les blocs texte vides + case "tool_use": + return [{ toolUse: { toolUseId: b.id, name: b.name, input: b.input } }]; + case "tool_result": + return [ + { + toolResult: { + toolUseId: b.toolUseId, + content: [{ text: b.content || "(vide)" }], + status: b.isError ? "error" : "success", + }, + }, + ]; + } + }), + })); + + const body: Record = { + system: [{ text: req.system }], + messages, + inferenceConfig: { maxTokens: req.maxTokens ?? 8192 }, + }; + if (req.tools.length > 0) { + body.toolConfig = { + tools: req.tools.map((t) => ({ + toolSpec: { + name: t.name, + description: t.description, + inputSchema: { json: t.inputSchema }, + }, + })), + }; + } + return body; +} + +interface ConverseResponse { + output?: { + message?: { + content?: { text?: string; toolUse?: { toolUseId: string; name: string; input?: Record } }[]; + }; + }; + stopReason?: string; +} + +export function parseConverseResponse(json: unknown): StreamEvent[] { + const res = json as ConverseResponse; + const events: StreamEvent[] = []; + for (const block of res.output?.message?.content ?? []) { + if (block.text) events.push({ type: "text_delta", text: block.text }); + if (block.toolUse) { + events.push({ + type: "tool_use", + id: block.toolUse.toolUseId, + name: block.toolUse.name, + input: block.toolUse.input ?? {}, + }); + } + } + const stop: StopReason = + res.stopReason === "end_turn" || res.stopReason === "tool_use" || res.stopReason === "max_tokens" + ? res.stopReason + : "other"; + events.push({ type: "done", stopReason: stop }); + return events; +} diff --git a/packages/providers/src/sapaicore/config.ts b/packages/providers/src/sapaicore/config.ts new file mode 100644 index 0000000..2b49531 --- /dev/null +++ b/packages/providers/src/sapaicore/config.ts @@ -0,0 +1,56 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +export interface BtpAiConfig { + /** URL de base pour récupérer le token (on y ajoute /oauth/token). */ + url: string; + /** Token Basic déjà encodé, envoyé dans le header Authorization. */ + basic: string; + serviceurls: { + /** URL de base de l'API AI Core (liste des déploiements). */ + AI_API_URL: string; + }; + /** Optionnel : resource group AI Core (défaut : "default"). */ + resourceGroup?: string; +} + +export const DEFAULT_CONFIG_PATH = "./btp_AI.json"; + +/** + * Charge et valide le fichier JSON de configuration du service AI + * (type SAP AI Core / Generative AI Hub). Voir btp_AI_tpl.json. + */ +export async function loadConfig(configPath: string, baseDir: string = process.cwd()): Promise { + // Chemin relatif : résolu depuis le dossier de lancement de MagicCode (baseDir) ; chemin complet : tel quel. + const absolutePath = resolve(baseDir, configPath); + + let raw: string; + try { + raw = await readFile(absolutePath, "utf-8"); + } catch (err) { + throw new Error( + `Impossible de lire le fichier de configuration "${absolutePath}" : ${(err as Error).message}`, + ); + } + + let config: Partial; + try { + config = JSON.parse(raw) as Partial; + } catch (err) { + throw new Error( + `Le fichier de configuration "${absolutePath}" n'est pas un JSON valide : ${(err as Error).message}`, + ); + } + + const missing: string[] = []; + if (!config.url) missing.push("url"); + if (!config.basic) missing.push("basic"); + if (!config.serviceurls?.AI_API_URL) missing.push("serviceurls.AI_API_URL"); + if (missing.length > 0) { + throw new Error( + `Champ(s) manquant(s) dans le fichier de configuration : ${missing.join(", ")}`, + ); + } + + return config as BtpAiConfig; +} diff --git a/packages/providers/src/sapaicore/deployments.ts b/packages/providers/src/sapaicore/deployments.ts new file mode 100644 index 0000000..cd8a266 --- /dev/null +++ b/packages/providers/src/sapaicore/deployments.ts @@ -0,0 +1,129 @@ +import type { BtpAiConfig } from "./config"; +import { safeReadText } from "./http"; + +export const DEFAULT_RESOURCE_GROUP = "default"; + +/** Famille de modèle : détermine l'API à appeler sur le déploiement. */ +export type ModelType = "claude" | "openai" | "other"; + +export interface Deployment { + id: string; + name: string; + version: string | null; + deploymentUrl: string; + type: ModelType; +} + +/** + * Hypothèse (la fonction n'était pas dans les samples) : + * - anthropic--* / claude → Claude (AWS Bedrock, API Converse) + * - gpt*, o1/o3/o4… → OpenAI (Azure OpenAI, chat/completions) + * - le reste (gemini, embeddings, mistral…) → non pris en charge pour l'instant + */ +export function detectModelType(name: string): ModelType { + const n = name.toLowerCase(); + if (n.startsWith("anthropic--") || n.includes("claude")) return "claude"; + if (/^(gpt|chatgpt|o\d)/.test(n)) return "openai"; + return "other"; +} + +/** + * Liste les modèles/déploiements disponibles. + * + * GET {AI_API_URL}/v2/lm/deployments + * Headers: Authorization: Bearer {token}, AI-Resource-Group + */ +export async function listDeployments(config: BtpAiConfig, token: string): Promise { + const url = `${config.serviceurls.AI_API_URL.replace(/\/+$/, "")}/v2/lm/deployments`; + + const response = await fetch(url, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + "AI-Resource-Group": config.resourceGroup ?? DEFAULT_RESOURCE_GROUP, + }, + }); + + if (!response.ok) { + const body = await safeReadText(response); + throw new Error( + `Échec de récupération de la liste des modèles (HTTP ${response.status} ${response.statusText}). ${body}`, + ); + } + + const data = (await response.json()) as { resources?: RawDeployment[] }; + const resources = Array.isArray(data.resources) ? data.resources : []; + + const deployments = resources + .filter((r) => r.deploymentUrl) + .map((r): Deployment => { + const model = extractModelInfo(r); + const name = model?.name ?? r.configurationName ?? r.id ?? "(nom inconnu)"; + return { + id: r.id ?? r.deploymentUrl!, + name, + version: model?.version ?? null, + deploymentUrl: r.deploymentUrl!, + type: detectModelType(name), + }; + }); + + if (deployments.length === 0) { + throw new Error( + "Aucun modèle déployé (avec deploymentUrl) n'a été trouvé pour ce service/resource group.", + ); + } + return deployments; +} + +/** + * Choisit le déploiement : nom/id exact, sinon nom partiel. Vide ou "auto" → premier + * déploiement de chat pris en charge. + */ +export function findDeployment(deployments: Deployment[], wanted: string): Deployment { + const w = wanted.trim().toLowerCase(); + const usable = deployments.filter((d) => d.type !== "other"); + + if (!w || w === "auto") { + const first = usable[0]; + if (!first) { + throw new Error("Aucun déploiement de chat pris en charge (Claude ou GPT) n'a été trouvé."); + } + return first; + } + + const found = + usable.find((d) => d.name.toLowerCase() === w || d.id.toLowerCase() === w) ?? + usable.find((d) => d.name.toLowerCase().includes(w)); + if (!found) { + throw new Error( + `Modèle "${wanted}" introuvable. Disponibles : ${usable.map((d) => d.name).join(", ") || "(aucun)"}`, + ); + } + return found; +} + +interface RawModel { + name?: string; + version?: string | null; +} +interface RawDeployment { + id?: string; + deploymentUrl?: string; + configurationName?: string; + details?: { resources?: { backend_details?: { model?: RawModel }; backendDetails?: { model?: RawModel } } }; + backend_details?: { model?: RawModel }; + backendDetails?: { model?: RawModel }; +} + +/** Chemin confirmé "details.resources.backend_details.model", avec variantes de repli. */ +function extractModelInfo(r: RawDeployment): { name: string; version: string | null } | null { + const candidates = [ + r.details?.resources?.backend_details?.model, + r.details?.resources?.backendDetails?.model, + r.backend_details?.model, + r.backendDetails?.model, + ]; + const found = candidates.find((c) => c?.name); + return found?.name ? { name: found.name, version: found.version ?? null } : null; +} diff --git a/packages/providers/src/sapaicore/http.ts b/packages/providers/src/sapaicore/http.ts new file mode 100644 index 0000000..5cca683 --- /dev/null +++ b/packages/providers/src/sapaicore/http.ts @@ -0,0 +1,44 @@ +export class HttpError extends Error { + constructor( + public readonly status: number, + message: string, + ) { + super(message); + } +} + +export async function safeReadText(response: Response): Promise { + try { + return await response.text(); + } catch { + return ""; + } +} + +/** POST JSON avec token Bearer et resource group AI Core. */ +export async function postJson( + url: string, + token: string, + resourceGroup: string, + body: unknown, + signal?: AbortSignal, +): Promise { + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "AI-Resource-Group": resourceGroup, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + signal, + }); + if (!response.ok) { + const text = await safeReadText(response); + throw new HttpError( + response.status, + `Échec de l'appel au modèle (HTTP ${response.status} ${response.statusText}). ${text}`, + ); + } + return response.json(); +} diff --git a/packages/providers/src/sapaicore/index.ts b/packages/providers/src/sapaicore/index.ts new file mode 100644 index 0000000..f2ba827 --- /dev/null +++ b/packages/providers/src/sapaicore/index.ts @@ -0,0 +1,106 @@ +import type { ChatRequest, LLMProvider, StreamEvent } from "@magiccode/core"; +import { createTokenProvider, type TokenProvider } from "./auth"; +import { buildConverseBody, parseConverseResponse } from "./claude"; +import { DEFAULT_CONFIG_PATH, loadConfig, type BtpAiConfig } from "./config"; +import { + DEFAULT_RESOURCE_GROUP, + findDeployment, + listDeployments, + type Deployment, +} from "./deployments"; +import { HttpError, postJson } from "./http"; +import { OPENAI_API_VERSION, buildChatBody, parseChatResponse } from "./openai"; + +export interface SapAiCoreOptions { + /** Chemin du JSON de config (défaut : $MAGICCODE_BTP_CONFIG ou ./btp_AI.json). */ + configPath?: string; + /** Dossier de lancement de MagicCode : base des chemins relatifs (défaut : process.cwd()). */ + baseDir?: string; +} + +function defaultConfigPath(opts?: SapAiCoreOptions): string { + return opts?.configPath ?? process.env.MAGICCODE_BTP_CONFIG ?? DEFAULT_CONFIG_PATH; +} + +interface Resolved { + config: BtpAiConfig; + tokens: TokenProvider; + deployment: Deployment; +} + +/** + * Provider SAP AI Core / Generative AI Hub. + * `modelName` : nom du modèle déployé (ex. "anthropic--claude-4.5-sonnet"), ou "" / "auto" + * pour prendre le premier déploiement de chat pris en charge. + * La connexion (config, token, déploiements) est faite à la première requête. + */ +export function createSapAiCoreProvider(modelName: string, opts?: SapAiCoreOptions): LLMProvider { + const configPath = defaultConfigPath(opts); + let ready: Promise | undefined; + let resolvedName: string | undefined; + + const resolve = (): Promise => { + if (!ready) { + const attempt = (async (): Promise => { + const config = await loadConfig(configPath, opts?.baseDir); + const tokens = createTokenProvider(config); + const deployments = await listDeployments(config, await tokens.get()); + const deployment = findDeployment(deployments, modelName); + resolvedName = deployment.name; + return { config, tokens, deployment }; + })(); + attempt.catch(() => { + ready = undefined; // permet de réessayer après une erreur + }); + ready = attempt; + } + return ready; + }; + + const send = async (ctx: Resolved, req: ChatRequest, signal?: AbortSignal): Promise => { + const token = await ctx.tokens.get(); + const group = ctx.config.resourceGroup ?? DEFAULT_RESOURCE_GROUP; + const base = ctx.deployment.deploymentUrl.replace(/\/+$/, ""); + + if (ctx.deployment.type === "claude") { + const json = await postJson(`${base}/converse`, token, group, buildConverseBody(req), signal); + return parseConverseResponse(json); + } + const url = `${base}/chat/completions?api-version=${OPENAI_API_VERSION}`; + const json = await postJson(url, token, group, buildChatBody(req), signal); + return parseChatResponse(json); + }; + + return { + id: "sapaicore", + get model(): string { + return resolvedName ?? (modelName || "auto"); + }, + capabilities: { tools: true }, + async *chat(req, signal) { + const ctx = await resolve(); + let events: StreamEvent[]; + try { + events = await send(ctx, req, signal); + } catch (err) { + if (err instanceof HttpError && err.status === 401) { + ctx.tokens.invalidate(); // token expiré : un seul nouvel essai + events = await send(ctx, req, signal); + } else { + throw err; + } + } + // TODO : streaming (converse-stream = flux binaire AWS event-stream ; chat/completions = SSE). + for (const ev of events) yield ev; + }, + }; +} + +/** Déploiements disponibles sur le service (pour le sélecteur de modèle). */ +export async function listSapDeployments(opts?: SapAiCoreOptions): Promise { + const config = await loadConfig(defaultConfigPath(opts), opts?.baseDir); + const tokens = createTokenProvider(config); + return listDeployments(config, await tokens.get()); +} + +export type { Deployment } from "./deployments"; diff --git a/packages/providers/src/sapaicore/openai.ts b/packages/providers/src/sapaicore/openai.ts new file mode 100644 index 0000000..11cc370 --- /dev/null +++ b/packages/providers/src/sapaicore/openai.ts @@ -0,0 +1,80 @@ +import type { ChatRequest, StopReason, StreamEvent } from "@magiccode/core"; + +// GPT sur AI Core = déploiement Azure OpenAI : POST {deploymentUrl}/chat/completions?api-version=… +// À ajuster selon la version d'API acceptée par votre service. +export const OPENAI_API_VERSION = "2024-10-21"; + +export function buildChatBody(req: ChatRequest): Record { + const messages: Record[] = [{ role: "system", content: req.system }]; + + for (const m of req.messages) { + if (m.role === "assistant") { + const text = m.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join(""); + const toolCalls = m.content.flatMap((b) => + b.type === "tool_use" + ? [{ id: b.id, type: "function", function: { name: b.name, arguments: JSON.stringify(b.input) } }] + : [], + ); + messages.push({ + role: "assistant", + content: text || null, + ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}), + }); + } else { + for (const b of m.content) { + if (b.type === "tool_result") { + messages.push({ role: "tool", tool_call_id: b.toolUseId, content: b.content }); + } else if (b.type === "text") { + messages.push({ role: "user", content: b.text }); + } + } + } + } + + const body: Record = { messages }; + if (req.tools.length > 0) { + body.tools = req.tools.map((t) => ({ + type: "function", + function: { name: t.name, description: t.description, parameters: t.inputSchema }, + })); + } + return body; +} + +interface ChatResponse { + choices?: { + finish_reason?: string; + message?: { + content?: string | null; + tool_calls?: { id: string; function: { name: string; arguments: string } }[]; + }; + }[]; +} + +export function parseChatResponse(json: unknown): StreamEvent[] { + const choice = (json as ChatResponse).choices?.[0]; + const events: StreamEvent[] = []; + const msg = choice?.message; + + if (msg?.content) events.push({ type: "text_delta", text: msg.content }); + for (const call of msg?.tool_calls ?? []) { + let input: Record = {}; + try { + input = JSON.parse(call.function.arguments || "{}") as Record; + } catch { + // arguments invalides : l'outil recevra un objet vide et renverra une erreur explicite + } + events.push({ type: "tool_use", id: call.id, name: call.function.name, input }); + } + + const stop: StopReason = + choice?.finish_reason === "tool_calls" + ? "tool_use" + : choice?.finish_reason === "stop" + ? "end_turn" + : choice?.finish_reason === "length" + ? "max_tokens" + : "other"; + events.push({ type: "done", stopReason: stop }); + return events; +} diff --git a/packages/providers/src/sapaicore/sapaicore.test.ts b/packages/providers/src/sapaicore/sapaicore.test.ts new file mode 100644 index 0000000..0f2e42e --- /dev/null +++ b/packages/providers/src/sapaicore/sapaicore.test.ts @@ -0,0 +1,78 @@ +import { mkdtemp, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { StreamEvent } from "@magiccode/core"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createSapAiCoreProvider } from "./index"; + +const json = (body: unknown) => + new Response(JSON.stringify(body), { status: 200, headers: { "Content-Type": "application/json" } }); + +afterEach(() => vi.unstubAllGlobals()); + +describe("provider sapaicore", () => { + it("s'authentifie, résout le déploiement Claude et appelle /converse", async () => { + const dir = await mkdtemp(join(tmpdir(), "magiccode-")); + const cfg = join(dir, "btp_AI.json"); + await writeFile( + cfg, + JSON.stringify({ + url: "https://auth.example", + basic: "abc", + serviceurls: { AI_API_URL: "https://api.example" }, + }), + ); + + vi.stubGlobal( + "fetch", + vi.fn(async (input: URL | string) => { + const url = String(input); + if (url.includes("/oauth/token")) return json({ access_token: "tok", expires_in: 3600 }); + if (url.endsWith("/v2/lm/deployments")) { + return json({ + resources: [ + { + id: "d1", + deploymentUrl: "https://api.example/v2/inference/deployments/d1", + details: { + resources: { + backend_details: { model: { name: "anthropic--claude-4.5-sonnet", version: "1" } }, + }, + }, + }, + ], + }); + } + if (url.endsWith("/d1/converse")) { + return json({ + output: { + message: { + content: [{ text: "salut" }, { toolUse: { toolUseId: "t1", name: "Read", input: { path: "a" } } }], + }, + }, + stopReason: "tool_use", + }); + } + throw new Error(`URL inattendue : ${url}`); + }), + ); + + // chemin relatif + dossier de lancement : indépendant du process.cwd() réel + const provider = createSapAiCoreProvider("", { configPath: "btp_AI.json", baseDir: dir }); + const events: StreamEvent[] = []; + for await (const ev of provider.chat({ + system: "s", + messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }], + tools: [], + })) { + events.push(ev); + } + + expect(events).toEqual([ + { type: "text_delta", text: "salut" }, + { type: "tool_use", id: "t1", name: "Read", input: { path: "a" } }, + { type: "done", stopReason: "tool_use" }, + ]); + expect(provider.model).toBe("anthropic--claude-4.5-sonnet"); + }); +}); diff --git a/packages/providers/tsconfig.json b/packages/providers/tsconfig.json new file mode 100644 index 0000000..564a599 --- /dev/null +++ b/packages/providers/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"] +} diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..4a1932c --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,12 @@ +{ + "name": "@magiccode/ui", + "version": "0.1.0", + "type": "module", + "main": "src/index.ts", + "types": "src/index.ts", + "scripts": { "typecheck": "tsc -p ." }, + "dependencies": { + "@clack/prompts": "^0.9.0", + "@magiccode/core": "workspace:*" + } +} diff --git a/packages/ui/src/clack-ui.ts b/packages/ui/src/clack-ui.ts new file mode 100644 index 0000000..e1acc0f --- /dev/null +++ b/packages/ui/src/clack-ui.ts @@ -0,0 +1,100 @@ +import * as p from "@clack/prompts"; +import type { AgentUI } from "@magiccode/core"; + +export interface Choice { + value: T; + label: string; + hint?: string; +} + +const PREVIEW_MAX = 600; + +export class ClackUI implements AgentUI { + private spin = p.spinner(); + private spinning = false; + + // ----- cycle de vie ----- + intro(title: string): void { + p.intro(title); + } + outro(message: string): void { + p.outro(message); + } + info(message: string): void { + p.log.info(message); + } + warn(message: string): void { + p.log.warn(message); + } + error(message: string): void { + p.log.error(message); + } + + /** Affiche un spinner pendant une opération asynchrone (chargement des modèles, etc.). */ + async withSpinner(message: string, fn: () => Promise, done = "Terminé"): Promise { + const s = p.spinner(); + s.start(message); + try { + const result = await fn(); + s.stop(done); + return result; + } catch (err) { + s.stop("Échec", 1); + throw err; + } + } + + // ----- saisies ----- + /** Retourne null si l'utilisateur annule (Ctrl+C). */ + async select(message: string, choices: Choice[], initialValue?: T): Promise { + const result = await p.select({ + message, + options: choices.map((c) => ({ value: c.value, label: c.label, hint: c.hint })) as never, + initialValue: initialValue as never, + }); + return p.isCancel(result) ? null : (result as T); + } + + async readInput(): Promise { + const result = await p.text({ + message: "Vous", + placeholder: "Votre demande (/help pour l'aide)", + }); + return p.isCancel(result) ? null : String(result).trim(); + } + + async confirm(message: string): Promise { + const result = await p.confirm({ message }); + return !p.isCancel(result) && result === true; + } + + // ----- AgentUI : affichage du déroulé de l'agent ----- + onThinking(active: boolean): void { + if (active && !this.spinning) { + this.spin.start("Réflexion…"); + this.spinning = true; + } else if (!active && this.spinning) { + this.spin.stop(""); + this.spinning = false; + } + } + + onText(text: string): void { + process.stdout.write(text); + } + + onTextEnd(): void { + process.stdout.write("\n"); + } + + onToolCall(name: string, summary: string): void { + p.log.step(`${name} ${summary}`); + } + + onToolResult(_name: string, output: string, isError: boolean): void { + const preview = + output.length > PREVIEW_MAX ? `${output.slice(0, PREVIEW_MAX)}\n… (tronqué)` : output; + if (isError) p.log.error(preview); + else if (preview) p.log.message(preview); + } +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts new file mode 100644 index 0000000..12838c3 --- /dev/null +++ b/packages/ui/src/index.ts @@ -0,0 +1 @@ +export * from "./clack-ui"; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json new file mode 100644 index 0000000..564a599 --- /dev/null +++ b/packages/ui/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..2fcf4b3 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2164 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.20.5 + tsup: + specifier: ^8.3.0 + version: 8.5.1(postcss@8.5.29)(tsx@4.23.15)(typescript@5.9.3) + tsx: + specifier: ^4.19.0 + version: 4.23.15 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@22.20.5) + + packages/cli: + dependencies: + '@anthropic-ai/sdk': + specifier: ^0.40.0 + version: 0.40.1 + '@clack/prompts': + specifier: ^0.9.0 + version: 0.9.1 + '@magiccode/core': + specifier: workspace:* + version: link:../core + '@magiccode/ext-claudecode': + specifier: workspace:* + version: link:../ext-claudecode + '@magiccode/providers': + specifier: workspace:* + version: link:../providers + '@magiccode/ui': + specifier: workspace:* + version: link:../ui + ignore: + specifier: ^6.0.2 + version: 6.0.2 + picomatch: + specifier: ^3.0.1 + version: 3.0.2 + devDependencies: + '@types/picomatch': + specifier: ^3.0.1 + version: 3.0.2 + + packages/core: {} + + packages/ext-claudecode: + dependencies: + '@magiccode/core': + specifier: workspace:* + version: link:../core + ignore: + specifier: ^6.0.2 + version: 6.0.2 + picomatch: + specifier: ^3.0.1 + version: 3.0.2 + devDependencies: + '@types/picomatch': + specifier: ^3.0.1 + version: 3.0.2 + + packages/providers: + dependencies: + '@anthropic-ai/sdk': + specifier: ^0.40.0 + version: 0.40.1 + '@magiccode/core': + specifier: workspace:* + version: link:../core + + packages/ui: + dependencies: + '@clack/prompts': + specifier: ^0.9.0 + version: 0.9.1 + '@magiccode/core': + specifier: workspace:* + version: link:../core + +packages: + + '@anthropic-ai/sdk@0.40.1': + resolution: {integrity: sha512-DJMWm8lTEM9Lk/MSFL+V+ugF7jKOn0M2Ujvb5fN8r2nY14aHbGPZ1k6sgjL+tpJ3VuOGJNG+4R83jEpOuYPv8w==} + + '@clack/core@0.4.1': + resolution: {integrity: sha512-Pxhij4UXg8KSr7rPek6Zowm+5M22rbd2g1nfojHJkxp5YkFqiZ2+YLEM/XGVIzvGOcM0nqjIFxrpDwWRZYWYjA==} + + '@clack/prompts@0.9.1': + resolution: {integrity: sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + + '@rollup/rollup-android-arm-eabi@4.64.5': + resolution: {integrity: sha512-MrfgRQSpD5hkkRg6XI16B6EDLb+6+xp8xje6bpdSwkYw+qxrBKbUVrRAQu9h82UT+QumSIVQ8HXsXjPd2dlhqw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.64.5': + resolution: {integrity: sha512-+uu2p/R9Tzn8GpoQyX1pAo5dmKT1DhXevfaCzqzmPFhnLjHLWw7uWkaVGlxmhwYoRiYYhsGfRcD5cTBpL7rgSw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.64.5': + resolution: {integrity: sha512-17dys+YEmzwzzFVHQAVDONg5FALXY9kd99HFEpfk4gJVHYxoh5bGea3D4t1fXXnwkTcOe25wPyd6nWS0I8IdLQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.64.5': + resolution: {integrity: sha512-wg0zQEKkEvFZf1UaaX1UELOoPs//OSKUG7D+yITuqOsAvza7LvHTv5Wu4nmMfzNG7EYI2jp2PJC7Id6OETmsYQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.64.5': + resolution: {integrity: sha512-CoJmWcpEMuwin+TZ/ev/H9uDyF/uxUKQSWXdmIPn/UBx/uGV9+4ebMp0FwnlhWR9PE1AdzYsKf3bZevhif38+A==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.64.5': + resolution: {integrity: sha512-PxZnLAaTUAL5xG0W2BXLeYPKrK2b3LIASV2/e/Kf7PZvSLI1iAQIm9B9aJD+YyFIT7hc6qaMV3+OOO35R28LfA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.64.5': + resolution: {integrity: sha512-uPo3IvnONjhbFXwWgroBeFwOicuhT7ZwQrnzkzmOtBJzeQbkUJUE1SJDfDI31ijQ5+ZUIA6sPrdUMxTSnxUWAg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.64.5': + resolution: {integrity: sha512-ywI8Jyuc4LyHYTjC0mw4yA1ulvZBkHusdpWnoiEBsmBuHvSnt90btTBGCZ+s/2y+YNNfVAEKy33nPo5UI9zTmg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.64.5': + resolution: {integrity: sha512-tmuXJoQ/3ovSr637aykZMdQteU/CB2APAz2CrvsbibuoHZi1BS1Db13cVViBdEARGwXCLn1DSJ8wsjoThLJPGA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.64.5': + resolution: {integrity: sha512-5FMjSYNcUOHx7oW8Qu9KB6jGPpvF/8sG7+BDHwU/Ihh1/6FMJ9ySmsFG4tJ1qbNRuP8OLzCBDso4sF0mQaLuFQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.64.5': + resolution: {integrity: sha512-K8ZJ+neLDbUkIrL8dqF/5LtuVCrKwo27kREgOlxjBEjlOXwHvDnHLU8Z/UKv/B49qi7e8hsSDOyoJwhwqhvX4w==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.64.5': + resolution: {integrity: sha512-NlVxsq7PFPv0DBKGuNWRDKixQYQCjM53zl04mxEcKMBmJbCBIUm1JdPfIWDRHyOOFzzYPmLQzfeEwh4716ayQg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.64.5': + resolution: {integrity: sha512-ARhRHJYs5eCFet5azfrHD97vOkD8OzNW2RSMlvo5CcBqXaTOLfp0OHDtUQh7/K5AL5fUYoFp2fgC9KDg4vVYMQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.64.5': + resolution: {integrity: sha512-atSgreM5C7jpxd9aEXN+6vJ5wTd05iYw+S3FiQpw7VsoEl8B90llbq35ARON+GOHj5QlVQEq2vOKWCHTob/BSQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.64.5': + resolution: {integrity: sha512-vmlmwBFGnjVWxSRoVabNkyYuscSmg52Pd0fM8jEbNT3Jmka4A6HcB84x4uAmuumznWfBVaxKOqU6jbEgomWKCA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.64.5': + resolution: {integrity: sha512-hhJrP6S9WkbsbcHgUkvCBS5Cn8IdsG8XjlLZyxjnv9+NdoYfbbZE4TWJYr8apdaj1M5t5Y1id2StnEslxpNFyA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.64.5': + resolution: {integrity: sha512-NNVCyN9pGrwFmQa4bf+Cnf5aVH3g6ZpnlOg1Cs/H9swIHT9bRixijfzUqyEIWm1WtiAuR1LJ+rJH6zpBm8kgQA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.64.5': + resolution: {integrity: sha512-dR9nd6OKnvSS60Ifz/L7Mj5zzYyoqyRn2NwTuaOONKaa3xsVGXyp8YusMbvFyEOR94DxnwgcbgsFqkHd29ZSzQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.64.5': + resolution: {integrity: sha512-1tzsMjaqhsh3bAO6SJiJqh8hIr9sSPRd+i2vPPxJHod8/XYnf9TEnK0z/u3r1z1AkI347X0KbRSg9dWcxeGahw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.64.5': + resolution: {integrity: sha512-01mWMQblKoXXsmk28KXtpbA96w7g/aOWUoV3AC16fdHdqkykGalqyyM+5u8M7uP5/MEMiiPv69Hv7iv/hEH/dA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.64.5': + resolution: {integrity: sha512-trdnMPb/DNm4V/CgsaoGgr90HfQQ071wfK0KoIs4YpLvjBgVQGXhGabg2Z6nK4EcYNilrb9ZZPcsP6+zpJIdxQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.64.5': + resolution: {integrity: sha512-x2LxZlfdfsw99T6/qb9EqE/Qi84JWzibjyOJyhJsgFJy6CafaTiqIXCCnslCGrV5XWNh7fwrS3FbRENcuKnooA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.64.5': + resolution: {integrity: sha512-/Qd/9aDQIo0+jmItgBmPIs8195YVbDxRRApn47GyufgbutCDOh7eP5vSKKlI4QNFeaI+wKQAkSMsPnF3EuMpaA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.64.5': + resolution: {integrity: sha512-ngUJ/XvOWrqbd2IMsfXfh2xXFLTP3sY7sCKvxbGvYdBZ1KmXFd3ewDZ9n3/GNzAUgzRuhLT/UvJRivbNoAQRBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.64.5': + resolution: {integrity: sha512-IhV6ew/DuzUWwHaYmlh7MrPCV3lKFUq/amTEwysi4r/mwNPAEfAMh5dYWeTSPTeMy2pFHGMEWYtPkHzfX3tG5A==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + + '@types/node@22.20.5': + resolution: {integrity: sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==} + + '@types/picomatch@3.0.2': + resolution: {integrity: sha512-n0i8TD3UDB7paoMMxA3Y65vUncFJXjcUf7lQY7YyKGl6031FNjfsLs6pdLFCy2GNFxItPJG8GvvpbZc2skH7WA==} + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + acorn@8.19.0: + resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + ignore@6.0.2: + resolution: {integrity: sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==} + engines: {node: '>= 4'} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@3.0.2: + resolution: {integrity: sha512-cfDHL6LStTEKlNilboNtobT/kEa30PtAf2Q1OgszfrG/rpVl1xaFWT9ktfkS306GmHgmnad1Sw4wabhlvFtsTw==} + engines: {node: '>=10'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss@8.5.29: + resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==} + engines: {node: ^10 || ^12 || >=14} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + rollup@4.64.5: + resolution: {integrity: sha512-EAP5ZHcz2ijbgCWwPkpI7zh6bfsri2Ez5FtCy2dCp1k62duXzfG7DLGGh7RU2QQXWwavT+u4qpvDF/TWOiiGTw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + tsx@4.23.15: + resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + +snapshots: + + '@anthropic-ai/sdk@0.40.1': + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + '@clack/core@0.4.1': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.9.1': + dependencies: + '@clack/core': 0.4.1 + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@rollup/rollup-android-arm-eabi@4.64.5': + optional: true + + '@rollup/rollup-android-arm64@4.64.5': + optional: true + + '@rollup/rollup-darwin-arm64@4.64.5': + optional: true + + '@rollup/rollup-darwin-x64@4.64.5': + optional: true + + '@rollup/rollup-freebsd-arm64@4.64.5': + optional: true + + '@rollup/rollup-freebsd-x64@4.64.5': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.64.5': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.64.5': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.64.5': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.64.5': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.64.5': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.64.5': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.64.5': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.64.5': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.64.5': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.64.5': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.64.5': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.64.5': + optional: true + + '@rollup/rollup-linux-x64-musl@4.64.5': + optional: true + + '@rollup/rollup-openbsd-x64@4.64.5': + optional: true + + '@rollup/rollup-openharmony-arm64@4.64.5': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.64.5': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.64.5': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.64.5': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.64.5': + optional: true + + '@types/estree@1.0.9': {} + + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 22.20.5 + form-data: 4.0.6 + + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + + '@types/node@22.20.5': + dependencies: + undici-types: 6.21.0 + + '@types/picomatch@3.0.2': {} + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.20.5))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@22.20.5) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + acorn@8.19.0: {} + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + any-promise@1.3.0: {} + + assertion-error@2.0.1: {} + + asynckit@0.4.0: {} + + bundle-require@5.1.0(esbuild@0.27.7): + dependencies: + esbuild: 0.27.7 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@4.1.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + delayed-stream@1.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + event-target-shim@5.0.1: {} + + expect-type@1.4.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.2 + rollup: 4.64.5 + + form-data-encoder@1.7.2: {} + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + ignore@6.0.2: {} + + joycon@3.1.1: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mlly@1.8.2: + dependencies: + acorn: 8.19.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.20: {} + + node-domexception@1.0.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + object-assign@4.1.1: {} + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@3.0.2: {} + + picomatch@4.0.7: {} + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + postcss-load-config@6.0.1(postcss@8.5.29)(tsx@4.23.15): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.29 + tsx: 4.23.15 + + postcss@8.5.29: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + readdirp@4.1.2: {} + + resolve-from@5.0.0: {} + + rollup@4.64.5: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.64.5 + '@rollup/rollup-android-arm64': 4.64.5 + '@rollup/rollup-darwin-arm64': 4.64.5 + '@rollup/rollup-darwin-x64': 4.64.5 + '@rollup/rollup-freebsd-arm64': 4.64.5 + '@rollup/rollup-freebsd-x64': 4.64.5 + '@rollup/rollup-linux-arm-gnueabihf': 4.64.5 + '@rollup/rollup-linux-arm-musleabihf': 4.64.5 + '@rollup/rollup-linux-arm64-gnu': 4.64.5 + '@rollup/rollup-linux-arm64-musl': 4.64.5 + '@rollup/rollup-linux-loong64-gnu': 4.64.5 + '@rollup/rollup-linux-loong64-musl': 4.64.5 + '@rollup/rollup-linux-ppc64-gnu': 4.64.5 + '@rollup/rollup-linux-ppc64-musl': 4.64.5 + '@rollup/rollup-linux-riscv64-gnu': 4.64.5 + '@rollup/rollup-linux-riscv64-musl': 4.64.5 + '@rollup/rollup-linux-s390x-gnu': 4.64.5 + '@rollup/rollup-linux-x64-gnu': 4.64.5 + '@rollup/rollup-linux-x64-musl': 4.64.5 + '@rollup/rollup-openbsd-x64': 4.64.5 + '@rollup/rollup-openharmony-arm64': 4.64.5 + '@rollup/rollup-win32-arm64-msvc': 4.64.5 + '@rollup/rollup-win32-ia32-msvc': 4.64.5 + '@rollup/rollup-win32-x64-gnu': 4.64.5 + '@rollup/rollup-win32-x64-msvc': 4.64.5 + fsevents: 2.3.3 + + siginfo@2.0.0: {} + + sisteransi@1.0.5: {} + + source-map-js@1.2.2: {} + + source-map@0.7.6: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.17 + ts-interface-checker: 0.1.13 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tr46@0.0.3: {} + + tree-kill@1.2.2: {} + + ts-interface-checker@0.1.13: {} + + tsup@8.5.1(postcss@8.5.29)(tsx@4.23.15)(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.7) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.7 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.29)(tsx@4.23.15) + resolve-from: 5.0.0 + rollup: 4.64.5 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.29 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsx@4.23.15: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + ufo@1.6.4: {} + + undici-types@5.26.5: {} + + undici-types@6.21.0: {} + + vite-node@2.1.9(@types/node@22.20.5): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@22.20.5) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@22.20.5): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.29 + rollup: 4.64.5 + optionalDependencies: + '@types/node': 22.20.5 + fsevents: 2.3.3 + + vitest@2.1.9(@types/node@22.20.5): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.20.5)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@22.20.5) + vite-node: 2.1.9(@types/node@22.20.5) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.5 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + web-streams-polyfill@4.0.0-beta.3: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..dee51e9 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "packages/*" diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..3899874 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noEmit": true, + "types": ["node"] + } +}