diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..dc690f1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "services/carbonyl/upstream"] + path = services/carbonyl/upstream + url = https://github.com/fathyb/carbonyl.git + shallow = true diff --git a/docs/API/anythingllm-devtools-api.md b/docs/API/anythingllm-devtools-api.md index eec5986..ab5c593 100644 --- a/docs/API/anythingllm-devtools-api.md +++ b/docs/API/anythingllm-devtools-api.md @@ -62,5 +62,5 @@ Erreurs `400` : corps JSON invalide ou message d’erreur métier (ex. repo intr ## Voir aussi - [anythingllm-workspaces.md](../anythingllm-workspaces.md) -- [extension-anythingllm-workspaces.md](../repo/extension-anythingllm-workspaces.md) (extension dépréciée côté surface IDE) +- [extension-anythingllm-workspaces.md](../repo/extension-anythingllm-workspaces.md) (extension IDE supprimée ; service HTTP uniquement) - [repos-devtools-server.md](./repos-devtools-server.md) diff --git a/docs/README.md b/docs/README.md index 7d51087..c8563be 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,7 +23,8 @@ Vue d’ensemble et index complet : **[repo/README.md](./repo/README.md)**. Règ | [repo/service-*.md](./repo/README.md) | Exploitation de chaque micro-service (voir index `repo/README`) | | [repo/script-anythingllm-pull-sync.md](./repo/script-anythingllm-pull-sync.md) | Hook post-merge → AnythingLLM | | [repo/service-anythingllm-devtools.md](./repo/service-anythingllm-devtools.md) | Service HTTP AnythingLLM + devtools | -| [repo/extension-anythingllm-workspaces.md](./repo/extension-anythingllm-workspaces.md) | Extension VS Code / Cursor (héritée) | +| [repo/service-carbonyl.md](./repo/service-carbonyl.md) | Carbonyl (navigateur terminal), prévisualisation test | +| [repo/extension-anythingllm-workspaces.md](./repo/extension-anythingllm-workspaces.md) | Extension AnythingLLM IDE (supprimée ; voir anythingllm-devtools) | Les fichiers **`README.md`** sous `services/*/`, `cron/`, `projects/`, etc. ne font que **renvoyer** vers ces pages. @@ -58,6 +59,7 @@ Les fichiers **`README.md`** sous `services/*/`, `cron/`, `projects/`, etc. ne f | Document | Contenu | |----------|---------| | [anythingllm-workspaces.md](./anythingllm-workspaces.md) | Un workspace AnythingLLM par projet, synchronisation | +| [features/carbonyl-terminal-browser.md](./features/carbonyl-terminal-browser.md) | Carbonyl, URL test dans `conf.json` | | [ux-navigation-model.md](./ux-navigation-model.md) | Intentions, recherche, mode expert | ## Intégration dépôts @@ -87,7 +89,7 @@ Les fichiers **`README.md`** sous `services/*/`, `cron/`, `projects/`, etc. ne f | [features/initial-rag-sync-4nkaiignore.md](./features/initial-rag-sync-4nkaiignore.md) | RAG initial et `.4nkaiignore` | | [features/ia-dev-service.md](./features/ia-dev-service.md) | Service `ia-dev-gateway`, fork `ia_dev`, migration | | [features/orchestrator-api.md](./features/orchestrator-api.md) | Contrat HTTP orchestrateur (Ollama, ALLM, services) | -| [features/lapce-porting-roadmap.md](./features/lapce-porting-roadmap.md) | Phases portage extension AnythingLLM → Lapce | +| [features/lapce-porting-roadmap.md](./features/lapce-porting-roadmap.md) | Phases portage surface AnythingLLM → Lapce (service HTTP + UI) | | [features/sso-docv-enso.md](./features/sso-docv-enso.md) | OIDC front ↔ docv (Enso) | | [features/docv-ai-integration.md](./features/docv-ai-integration.md) | Backend docv : API IA smart_ide, clones `../projects/`, AnythingLLM | | [features/docv-service-integration.md](./features/docv-service-integration.md) | docv gestion documentaire, `../projects//data`, `DOCV_PROJECTS_ROOT`, multi-hôte | diff --git a/docs/features/carbonyl-terminal-browser.md b/docs/features/carbonyl-terminal-browser.md new file mode 100644 index 0000000..55977ce --- /dev/null +++ b/docs/features/carbonyl-terminal-browser.md @@ -0,0 +1,32 @@ +# Carbonyl — navigateur terminal pour prévisualisation test + +## Contexte + +[Carbonyl](https://github.com/fathyb/carbonyl) rend une page web dans le terminal (moteur Chromium). Dans smart_ide, il sert à **consulter** des URLs (dont les déploiements **test** des projets) sans dépendre d’un navigateur graphique sur le poste. + +## Intégration monorepo + +- Répertoire : **`services/carbonyl/`** — sous-module **`upstream/`** (dépôt amont), script **`run-carbonyl.sh`**. +- Lancement ciblé test : **`scripts/open-carbonyl-preview-test.sh`** depuis la racine du monorepo. + +## Configuration projet + +Dans **`projects//conf.json`**, objet optionnel : + +```json +"smart_ide": { + "preview_urls": { + "test": "https://votre-app.test/" + } +} +``` + +Schéma général : [repo/ia-dev-project-conf-schema.md](../repo/ia-dev-project-conf-schema.md). + +## Limiter la volumétrie Git + +Ne pas initialiser récursivement les sous-modules **chromium** du dépôt Carbonyl sauf besoin de **compiler** le runtime. Pour l’usage courant : **Docker** (`fathyb/carbonyl`) ou paquet **`carbonyl`** npm. + +## Documentation liée + +- [repo/service-carbonyl.md](../repo/service-carbonyl.md) diff --git a/docs/features/initial-rag-sync-4nkaiignore.md b/docs/features/initial-rag-sync-4nkaiignore.md index da056aa..6beaaf6 100644 --- a/docs/features/initial-rag-sync-4nkaiignore.md +++ b/docs/features/initial-rag-sync-4nkaiignore.md @@ -9,7 +9,7 @@ ## Comportement 1. **Serveur `repos-devtools-server`** : après `git clone` réussi, copie **`templates/4nkaiignore.default`** vers **`/.4nkaiignore`** si absent. -2. **Service `anythingllm-devtools`** (ou extension héritée) : après `/repos-clone-sync`, `/repos-load-sync`, ou sur **`/workspace-sync `**, si la synchro initiale n’est pas désactivée (`ANYTHINGLLM_INITIAL_SYNC_AFTER_CLONE` côté service, ou `anythingllm.initialSyncAfterClone` côté extension) : +2. **Service `anythingllm-devtools`** : après `/repos-clone-sync`, `/repos-load-sync`, ou sur **`/workspace-sync `**, si la synchro initiale n’est pas désactivée (`ANYTHINGLLM_INITIAL_SYNC_AFTER_CLONE` côté service) : - assure **`.4nkaiignore`** depuis le template si toujours absent ; - parcourt le dépôt, applique règles de base + `.4nkaiignore` ; - envoie chaque fichier accepté via **`POST /api/v1/document/upload`** avec **`addToWorkspaces`** = slug du workspace. @@ -18,7 +18,6 @@ - **`services/anythingllm-devtools/templates/4nkaiignore.default`** (référence) - **`services/repos-devtools-server/templates/4nkaiignore.default`** (même contenu ; à maintenir en parité) -- **`extensions/anythingllm-workspaces/templates/4nkaiignore.default`** (copie héritée) L’utilisateur renomme / copie en **`.4nkaiignore`** à la racine du projet et adapte les règles. @@ -33,4 +32,4 @@ Le **collecteur / processeur de documents** doit être joignable par l’instanc ## Modalités de déploiement -- Rebuild et redémarrage de **repos-devtools-server** et de **anythingllm-devtools** ; repackaging / réinstallation de l’extension uniquement si vous conservez encore la surface IDE. +- Rebuild et redémarrage de **repos-devtools-server** et de **anythingllm-devtools**. diff --git a/docs/features/lapce-porting-roadmap.md b/docs/features/lapce-porting-roadmap.md index 9df4219..6b001ae 100644 --- a/docs/features/lapce-porting-roadmap.md +++ b/docs/features/lapce-porting-roadmap.md @@ -1,6 +1,6 @@ # Portage AnythingLLM Workspaces → Lapce (`core_ide/`) -L’orchestration AnythingLLM + repos-devtools est exposée en **service HTTP** [`services/anythingllm-devtools/`](../../services/anythingllm-devtools/) ; l’extension [extensions/anythingllm-workspaces/](../../extensions/anythingllm-workspaces/) reste une surface **VS Code / Cursor** héritée. Lapce utilise un **modèle de plugins** distinct (Volt / WASI, RPC). Ce document découpe le travail en **phases** pour une interface cohérente avec [platform-target.md](../platform-target.md). +L’orchestration AnythingLLM + repos-devtools est exposée en **service HTTP** [`services/anythingllm-devtools/`](../../services/anythingllm-devtools/). L’ancienne extension VS Code / Cursor a été retirée du dépôt. Lapce utilise un **modèle de plugins** distinct (Volt / WASI, RPC). Ce document découpe le travail en **phases** pour une interface cohérente avec [platform-target.md](../platform-target.md). ## Phase 1 — Connectivité sans webview @@ -13,7 +13,7 @@ L’orchestration AnythingLLM + repos-devtools est exposée en **service HTTP** ## Phase 2 — Parité « Dev tools » et sync RAG -- Panneau ou vue dédiée **ou** proxy vers le service : mêmes lignes de commande que `POST /v1/devtools/run` — [repo/extension-anythingllm-workspaces.md](../repo/extension-anythingllm-workspaces.md), [repo/service-anythingllm-devtools.md](../repo/service-anythingllm-devtools.md). +- Panneau ou vue dédiée **ou** proxy vers le service : mêmes lignes de commande que `POST /v1/devtools/run` — [repo/service-anythingllm-devtools.md](../repo/service-anythingllm-devtools.md). - Réutiliser le service Node existant **ou** réimplémenter **initialRagSync** + `.4nkaiignore` (crate `ignore` ou équivalent Rust). - Ouvrir le dossier dépôt dans Lapce après clone (API workspace Lapce). diff --git a/docs/repo/README.md b/docs/repo/README.md index 4c5996b..ed8aa7f 100644 --- a/docs/repo/README.md +++ b/docs/repo/README.md @@ -37,6 +37,7 @@ Toute la documentation **opérationnelle** qui vivait auparavant sous des `READM | [service-langextract.md](./service-langextract.md) | Wrapper LangExtract | | **Scripts et extensions** | | | [script-anythingllm-pull-sync.md](./script-anythingllm-pull-sync.md) | Hook post-merge → upload AnythingLLM | -| [extension-anythingllm-workspaces.md](./extension-anythingllm-workspaces.md) | Extension VS Code / Cursor AnythingLLM (héritée) | +| [service-carbonyl.md](./service-carbonyl.md) | Carbonyl (navigateur terminal), sous-module amont | +| [extension-anythingllm-workspaces.md](./extension-anythingllm-workspaces.md) | Extension AnythingLLM IDE (supprimée ; anythingllm-devtools) | Les **spécifications** détaillées (contrats HTTP, sécurité, orchestration) restent dans [../API/README.md](../API/README.md) et [../features/](../features/). diff --git a/docs/repo/extension-anythingllm-workspaces.md b/docs/repo/extension-anythingllm-workspaces.md index e9fda2d..8e0b2c8 100644 --- a/docs/repo/extension-anythingllm-workspaces.md +++ b/docs/repo/extension-anythingllm-workspaces.md @@ -1,45 +1,5 @@ -# Extension AnythingLLM (`extensions/anythingllm-workspaces/`) +# Extension AnythingLLM (supprimée) -**Surface préférée** : le service HTTP [`services/anythingllm-devtools/`](../../services/anythingllm-devtools/) — même orchestration (repos-devtools, workspaces AnythingLLM, upload RAG initial `.4nkaiignore`) ; l’orchestrateur, les agents et les scripts appellent ce service plutôt que l’IDE. +L’extension VS Code / Cursor **`extensions/anythingllm-workspaces`** a été **retirée** du dépôt. -Extension **VS Code / Cursor** (héritée) : API développeur AnythingLLM, **repos-devtools-server** optionnel, panneau **Dev tools**, mêmes commandes que `POST /v1/devtools/run` côté service. - -## Prérequis - -- AnythingLLM avec **clé API développeur** (ne pas confondre avec le Bearer nginx pour `/ollama/`). -- **repos-devtools-server** sur l’hôte qui possède les clones (défaut `http://127.0.0.1:37140`) — [service-repos-devtools.md](./service-repos-devtools.md). -- Processeur de documents en ligne pour les uploads. - -## Paramètres (settings) - -| Clé | Rôle | -|-----|------| -| `anythingllm.baseUrl` | URL AnythingLLM sans `/` final | -| `anythingllm.apiKey` | Clé API (settings utilisateur) | -| `anythingllm.reposApiBaseUrl` | URL repos-devtools | -| `anythingllm.reposApiToken` | Identique à `REPOS_DEVTOOLS_TOKEN` | -| `anythingllm.initialSyncAfterClone` | Défaut activé : upload après clone/load/workspace-sync | -| `anythingllm.initialSyncMaxFiles` | Défaut `400` | -| `anythingllm.initialSyncMaxFileBytes` | Défaut `5242880` | - -## Commandes (palette) - -List workspaces, ouvrir UI web, panneau Dev tools, lignes de commande scriptées (`/repos-clone-sync`, `/repos-load-sync`, `/workspace-sync`, …). - -## `.4nkaiignore` - -- Modèle canonique : `services/anythingllm-devtools/templates/4nkaiignore.default` (aligné avec repos-devtools-server) ; copie sous l’extension conservée pour compatibilité. -- À la racine du dépôt cible : fichier **`.4nkaiignore`**. -- Filtrage : paquet **`ignore`** (sémantique gitignore) + règles de base (`.git/`, `node_modules/`, …). - -## Build - -```bash -cd extensions/anythingllm-workspaces -npm install -npm run compile -``` - -## Références - -API documents AnythingLLM amont ; [anythingllm-workspaces.md](../anythingllm-workspaces.md). +Toute l’orchestration AnythingLLM + repos-devtools + RAG initial (`.4nkaiignore`) passe par le service HTTP **[`services/anythingllm-devtools/`](../../services/anythingllm-devtools/)** — voir [service-anythingllm-devtools.md](./service-anythingllm-devtools.md) et [API/anythingllm-devtools-api.md](../API/anythingllm-devtools-api.md). diff --git a/docs/repo/ia-dev-project-conf-schema.md b/docs/repo/ia-dev-project-conf-schema.md index f1e2a33..4201ded 100644 --- a/docs/repo/ia-dev-project-conf-schema.md +++ b/docs/repo/ia-dev-project-conf-schema.md @@ -38,7 +38,7 @@ Les agents ne modifient pas `projects//conf.json` sans validation humaine ex | `deploy.host_stays_on_test` | no | Comportement `deploy-by-script-to.sh` (branche test vs pprod/prod). | | `tickets` | no | URL issues, `authorized_emails` ; le **to** sert à résoudre le projet. | | `cron` | no | Extension **smart_ide** : `{ "git_pull": false }` pour désactiver le pull planifié. | -| `smart_ide` | no | Extension **smart_ide** : `remote_data_access`, `anythingllm_workspace_slug`, `workspace` (`folders` + `settings`, équivalent `.code-workspace` ; y placer `smartIde.activeProjectId`), etc. | +| `smart_ide` | no | Extension **smart_ide** : `remote_data_access`, `anythingllm_workspace_slug`, `workspace` (`folders` + `settings`, équivalent `.code-workspace` ; y placer `smartIde.activeProjectId`), `preview_urls` (`test`, … URLs pour prévisualisation ex. Carbonyl), etc. | Détail ticketing : `ia_dev/projects/ia_dev/docs/TICKETS_SPOOL_FORMAT.md`. diff --git a/docs/repo/projects-directory.md b/docs/repo/projects-directory.md index e03845e..2d90c32 100644 --- a/docs/repo/projects-directory.md +++ b/docs/repo/projects-directory.md @@ -48,7 +48,7 @@ Spécification : [features/remote-deployed-data-ssh.md](../features/remote-deplo ### Bloc optionnel `smart_ide` dans `conf.json` -Clé **`smart_ide`** avec notamment **`remote_data_access`**, **`anythingllm_workspace_slug`**, **`workspace`** (équivalent `.code-workspace` : `folders` + `settings.smartIde.activeProjectId`). Exemple : `projects/enso/conf.json`. +Clé **`smart_ide`** avec notamment **`remote_data_access`**, **`anythingllm_workspace_slug`**, **`workspace`** (équivalent `.code-workspace` : `folders` + `settings.smartIde.activeProjectId`), **`preview_urls`** (ex. **`test`** : URL du déploiement test pour [Carbonyl](../features/carbonyl-terminal-browser.md)). Exemple : `projects/enso/conf.json`. ### Projet actif pour l’éditeur / Cursor diff --git a/docs/repo/script-anythingllm-pull-sync.md b/docs/repo/script-anythingllm-pull-sync.md index 91b073c..0c6a3a8 100644 --- a/docs/repo/script-anythingllm-pull-sync.md +++ b/docs/repo/script-anythingllm-pull-sync.md @@ -48,4 +48,4 @@ rm -f /path/vers/repo/.git/hooks/post-merge ## Liens -[features/anythingllm-pull-sync-after-pull.md](../features/anythingllm-pull-sync-after-pull.md), [anythingllm-workspaces.md](../anythingllm-workspaces.md), [extension-anythingllm-workspaces.md](./extension-anythingllm-workspaces.md). +[features/anythingllm-pull-sync-after-pull.md](../features/anythingllm-pull-sync-after-pull.md), [anythingllm-workspaces.md](../anythingllm-workspaces.md), [service-anythingllm-devtools.md](./service-anythingllm-devtools.md). diff --git a/docs/repo/service-carbonyl.md b/docs/repo/service-carbonyl.md new file mode 100644 index 0000000..c9e62ce --- /dev/null +++ b/docs/repo/service-carbonyl.md @@ -0,0 +1,19 @@ +# Service Carbonyl (`services/carbonyl/`) + +Navigateur **terminal** basé sur Chromium — amont **[fathyb/carbonyl](https://github.com/fathyb/carbonyl)**, intégré au monorepo via le sous-module **`services/carbonyl/upstream/`**. + +## Rôle + +- **Pilotage / visualisation** d’URLs dans un terminal (SSH sans GUI, session locale). +- **Prévisualisation** des applications déployées en **test** : URL optionnelle dans **`projects//conf.json`** → **`smart_ide.preview_urls.test`**, ouverte par **`scripts/open-carbonyl-preview-test.sh`**. + +Ce n’est **pas** un service HTTP : pas de port d’écoute dans smart_ide. L’exécution est un **processus interactif** (Docker ou binaire `carbonyl`). + +## Exploitation + +Voir **[`services/carbonyl/README.md`](../../services/carbonyl/README.md)** et **[features/carbonyl-terminal-browser.md](../features/carbonyl-terminal-browser.md)**. + +## Voir aussi + +- [anythingllm-devtools](service-anythingllm-devtools.md) — orchestration AnythingLLM (HTTP), distincte de la navigation terminal. +- [browser-automation-criteria.md](../features/browser-automation-criteria.md) — critères pour un futur `browser-automation-api` (automatisation headless). diff --git a/docs/repo/service-repos-devtools.md b/docs/repo/service-repos-devtools.md index 584fdd8..e422100 100644 --- a/docs/repo/service-repos-devtools.md +++ b/docs/repo/service-repos-devtools.md @@ -33,7 +33,7 @@ Unité systemd utilisateur possible : `systemctl --user daemon-reload && systemc ## Templates -Maintenir **`templates/4nkaiignore.default`** aligné avec `services/anythingllm-devtools/templates/4nkaiignore.default` (et, pour compatibilité, `extensions/anythingllm-workspaces/templates/4nkaiignore.default`). +Maintenir **`templates/4nkaiignore.default`** aligné avec `services/anythingllm-devtools/templates/4nkaiignore.default`. ## Spécification HTTP diff --git a/docs/services.md b/docs/services.md index 6966631..f517452 100644 --- a/docs/services.md +++ b/docs/services.md @@ -34,7 +34,7 @@ Ce document décrit les **services logiciels** typiques sur l’**hôte** (serve ## Micro-services HTTP sous `services/` -Services d’appoint sur **`127.0.0.1`** (souvent auth **Bearer**) : Git devtools, **anythingllm-devtools** (AnythingLLM + RAG initial), LangExtract, recherche regex, proxy claw, **`ia-dev-gateway`** (agents / runs stub), **`smart-ide-orchestrator`** (routage intentions) — voir tableau dans [system-architecture.md](./system-architecture.md), la **référence API** dans [`API/README.md`](./API/README.md), et l’index d’exploitation [repo/README.md](./repo/README.md) (fichiers `repo/service-*.md`). +Services d’appoint sur **`127.0.0.1`** (souvent auth **Bearer**) : Git devtools, **anythingllm-devtools** (AnythingLLM + RAG initial), LangExtract, recherche regex, proxy claw, **`ia-dev-gateway`** (agents / runs stub), **`smart-ide-orchestrator`** (routage intentions) — voir tableau dans [system-architecture.md](./system-architecture.md), la **référence API** dans [`API/README.md`](./API/README.md), et l’index d’exploitation [repo/README.md](./repo/README.md) (fichiers `repo/service-*.md`). **Carbonyl** (`services/carbonyl/`) n’est pas un listener HTTP : navigateur terminal pour prévisualiser des URLs (ex. déploiement test) — [repo/service-carbonyl.md](./repo/service-carbonyl.md). ## Documentation liée diff --git a/docs/system-architecture.md b/docs/system-architecture.md index e54b433..4c6eba9 100644 --- a/docs/system-architecture.md +++ b/docs/system-architecture.md @@ -13,7 +13,7 @@ Vue produit multi-environnements, SSO et option navigateur : [platform-target.md ## Monorepo unique -Le **référentiel de vérité** pour l’écosystème décrit ici est **un seul dépôt Git** (`smart_ide`) : specs, services locaux, scripts, extensions, documentation, et arborescence éditeur vendue. **Les produits et livrables 4NK ne sont pas hébergés sur GitHub** ; la forge canonique est **interne** (ex. Gitea). Les dépôts publics (Lapce, bibliothèques Python, etc.) ne sont que des **amonts** éventuels pour import ou relecture, pas des cibles de publication obligatoires. +Le **référentiel de vérité** pour l’écosystème décrit ici est **un seul dépôt Git** (`smart_ide`) : specs, services locaux, scripts, documentation, et arborescence éditeur vendue. **Les produits et livrables 4NK ne sont pas hébergés sur GitHub** ; la forge canonique est **interne** (ex. Gitea). Les dépôts publics (Lapce, bibliothèques Python, etc.) ne sont que des **amonts** éventuels pour import ou relecture, pas des cibles de publication obligatoires. Conséquences : @@ -33,7 +33,7 @@ Conséquences : | `services/repos-devtools-server/` | **Outillage Git** HTTP local (clone, liste, chargement de dépôts sous racine contrôlée) | | `core_ide/` | **Sources Lapce** — socle applicatif (build éditeur, personnalisations) — clone amont, hors index du parent | | `services/anythingllm-devtools/` | HTTP : AnythingLLM + repos-devtools + RAG initial (`.4nkaiignore`) — [API/anythingllm-devtools-api.md](./API/anythingllm-devtools-api.md) | -| `extensions/anythingllm-workspaces/` | Extension VS Code / Cursor (héritée) ; surface préférée : service **anythingllm-devtools** | +| `services/carbonyl/` | Navigateur terminal Chromium ([Carbonyl](https://github.com/fathyb/carbonyl)) ; sous-module **`upstream/`** ; prévisualisation test — [repo/service-carbonyl.md](./repo/service-carbonyl.md) | | `scripts/` , `setup/` , `systemd/` | Installation hôte, scripts d’exploitation, unités utilisateur pour services | | `cron/` | Pull **Git** planifié des clones décrits par `projects//conf.json` (`project_path`) — [repo/cron-git-pull.md](./repo/cron-git-pull.md) | | `services/local-office/` | **API REST** Office (upload, commandes docx, stockage SQLite + fichiers) ; complément programmatique à ONLYOFFICE | diff --git a/extensions/anythingllm-workspaces/.gitignore b/extensions/anythingllm-workspaces/.gitignore deleted file mode 100644 index d3e15b1..0000000 --- a/extensions/anythingllm-workspaces/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules/ -out/ -*.vsix diff --git a/extensions/anythingllm-workspaces/.vscodeignore b/extensions/anythingllm-workspaces/.vscodeignore deleted file mode 100644 index ebb513a..0000000 --- a/extensions/anythingllm-workspaces/.vscodeignore +++ /dev/null @@ -1,5 +0,0 @@ -.vscode/** -src/** -tsconfig.json -.gitignore -**/*.map diff --git a/extensions/anythingllm-workspaces/README.md b/extensions/anythingllm-workspaces/README.md deleted file mode 100644 index 7bae8b6..0000000 --- a/extensions/anythingllm-workspaces/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# AnythingLLM Workspaces (extension, héritée) - -Surface préférée : **[`services/anythingllm-devtools/`](../../services/anythingllm-devtools/)** (HTTP, orchestration identique). - -**[docs/repo/extension-anythingllm-workspaces.md](../../docs/repo/extension-anythingllm-workspaces.md)** diff --git a/extensions/anythingllm-workspaces/media/devTools.js b/extensions/anythingllm-workspaces/media/devTools.js deleted file mode 100644 index d0f3828..0000000 --- a/extensions/anythingllm-workspaces/media/devTools.js +++ /dev/null @@ -1,25 +0,0 @@ -(function () { - const vscode = acquireVsCodeApi(); - const input = document.getElementById("cmd"); - const out = document.getElementById("out"); - const runBtn = document.getElementById("run"); - const clearBtn = document.getElementById("clear"); - - function run() { - const text = input.value; - out.textContent = "…"; - vscode.postMessage({ type: "run", text: text }); - } - - runBtn.addEventListener("click", run); - clearBtn.addEventListener("click", function () { - out.textContent = ""; - }); - - window.addEventListener("message", function (event) { - const msg = event.data; - if (msg && msg.type === "result") { - out.textContent = typeof msg.text === "string" ? msg.text : String(msg.text); - } - }); -})(); diff --git a/extensions/anythingllm-workspaces/package-lock.json b/extensions/anythingllm-workspaces/package-lock.json deleted file mode 100644 index 99f2743..0000000 --- a/extensions/anythingllm-workspaces/package-lock.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "name": "anythingllm-workspaces", - "version": "0.3.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "anythingllm-workspaces", - "version": "0.3.0", - "license": "MIT", - "dependencies": { - "ignore": "^5.3.2" - }, - "devDependencies": { - "@types/node": "^20.11.0", - "@types/vscode": "^1.85.0", - "typescript": "^5.3.3" - }, - "engines": { - "vscode": "^1.85.0" - } - }, - "node_modules/@types/node": { - "version": "20.19.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", - "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", - "dev": true, - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/vscode": { - "version": "1.110.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", - "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", - "dev": true - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true - } - } -} diff --git a/extensions/anythingllm-workspaces/package.json b/extensions/anythingllm-workspaces/package.json deleted file mode 100644 index a934ad5..0000000 --- a/extensions/anythingllm-workspaces/package.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "name": "anythingllm-workspaces", - "displayName": "AnythingLLM Workspaces (ia.enso)", - "description": "AnythingLLM API, repos devtools, initial RAG sync via .4nkaiignore.", - "version": "0.3.0", - "publisher": "4nk", - "license": "MIT", - "engines": { - "vscode": "^1.85.0" - }, - "categories": [ - "Other" - ], - "activationEvents": [ - "onStartupFinished" - ], - "main": "./out/extension.js", - "contributes": { - "configuration": { - "title": "AnythingLLM", - "properties": { - "anythingllm.baseUrl": { - "type": "string", - "default": "https://ia.enso.4nkweb.com/anythingllm", - "markdownDescription": "Public base URL of AnythingLLM (no trailing slash)." - }, - "anythingllm.apiKey": { - "type": "string", - "default": "", - "markdownDescription": "AnythingLLM API key (**Settings → API Keys**). **User** settings." - }, - "anythingllm.reposApiBaseUrl": { - "type": "string", - "default": "http://127.0.0.1:37140", - "markdownDescription": "repos-devtools-server base URL (no trailing slash)." - }, - "anythingllm.reposApiToken": { - "type": "string", - "default": "", - "markdownDescription": "Same as `REPOS_DEVTOOLS_TOKEN` on the server." - }, - "anythingllm.initialSyncAfterClone": { - "type": "boolean", - "default": true, - "markdownDescription": "After `/repos-clone-sync` or `/repos-load-sync`, upload repo files to the workspace (filtered by `.4nkaiignore`). Requires AnythingLLM document processor (collector) online." - }, - "anythingllm.initialSyncMaxFiles": { - "type": "number", - "default": 400, - "minimum": 1, - "maximum": 10000, - "markdownDescription": "Max files to upload per initial sync." - }, - "anythingllm.initialSyncMaxFileBytes": { - "type": "number", - "default": 5242880, - "minimum": 1024, - "maximum": 104857600, - "markdownDescription": "Max size per file (bytes) for initial sync." - } - } - }, - "commands": [ - { - "command": "anythingllm.listWorkspaces", - "title": "AnythingLLM: List workspaces" - }, - { - "command": "anythingllm.openWebUi", - "title": "AnythingLLM: Open web UI" - }, - { - "command": "anythingllm.openDevToolsPanel", - "title": "AnythingLLM: Dev tools panel" - } - ] - }, - "scripts": { - "compile": "tsc -p ./", - "watch": "tsc -watch -p ./", - "vscode:prepublish": "npm run compile" - }, - "dependencies": { - "ignore": "^5.3.2" - }, - "devDependencies": { - "@types/node": "^20.11.0", - "@types/vscode": "^1.85.0", - "typescript": "^5.3.3" - } -} diff --git a/extensions/anythingllm-workspaces/src/anythingllmClient.ts b/extensions/anythingllm-workspaces/src/anythingllmClient.ts deleted file mode 100644 index fd7b793..0000000 --- a/extensions/anythingllm-workspaces/src/anythingllmClient.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { AnythingWorkspace } from "./types"; - -const trimTrailingSlashes = (value: string): string => value.replace(/\/+$/, ""); - -export const normalizeAnythingLlmBaseUrl = (raw: string): string => { - const trimmed = raw.trim(); - if (trimmed.length === 0) { - throw new Error("anythingllm.baseUrl is empty"); - } - return trimTrailingSlashes(trimmed); -}; - -const parseJson = (text: string): unknown => { - try { - return JSON.parse(text) as unknown; - } catch (cause) { - throw new Error("Invalid JSON from AnythingLLM API", { cause }); - } -}; - -const isRecord = (value: unknown): value is Record => - typeof value === "object" && value !== null && !Array.isArray(value); - -const isWorkspace = (value: unknown): value is AnythingWorkspace => { - if (!isRecord(value)) { - return false; - } - const id = value.id; - const name = value.name; - const slug = value.slug; - return typeof id === "number" && typeof name === "string" && typeof slug === "string"; -}; - -const parseListWorkspaces = (payload: unknown): readonly AnythingWorkspace[] => { - if (!isRecord(payload)) { - throw new Error("AnythingLLM API: expected object body"); - } - const list = payload.workspaces; - if (!Array.isArray(list)) { - throw new Error("AnythingLLM API: missing workspaces array"); - } - const workspaces: AnythingWorkspace[] = []; - for (const item of list) { - if (!isWorkspace(item)) { - throw new Error("AnythingLLM API: invalid workspace entry"); - } - workspaces.push(item); - } - return workspaces; -}; - -const normalizeApiSecret = (raw: string): string => { - const trimmed = raw.trim(); - const bearerPrefix = /^Bearer\s+/i; - return bearerPrefix.test(trimmed) ? trimmed.replace(bearerPrefix, "").trim() : trimmed; -}; - -const parseWorkspaceEnvelope = (payload: unknown): AnythingWorkspace => { - if (!isRecord(payload)) { - throw new Error("AnythingLLM API: expected object body"); - } - const ws = payload.workspace; - if (!isWorkspace(ws)) { - throw new Error("AnythingLLM API: missing workspace in response"); - } - return ws; -}; - -export const listWorkspaces = async ( - baseUrl: string, - apiKey: string, -): Promise => { - const normalized = normalizeAnythingLlmBaseUrl(baseUrl); - const key = normalizeApiSecret(apiKey); - if (key.length === 0) { - throw new Error("anythingllm.apiKey is empty"); - } - const url = `${normalized}/api/v1/workspaces`; - const response = await fetch(url, { - method: "GET", - headers: { - Accept: "application/json", - Authorization: `Bearer ${key}`, - }, - }); - const text = await response.text(); - if (!response.ok) { - throw new Error(`AnythingLLM API ${response.status}: ${text.slice(0, 500)}`); - } - return parseListWorkspaces(parseJson(text)); -}; - -export const createWorkspace = async ( - baseUrl: string, - apiKey: string, - name: string, -): Promise => { - const normalized = normalizeAnythingLlmBaseUrl(baseUrl); - const key = normalizeApiSecret(apiKey); - if (key.length === 0) { - throw new Error("anythingllm.apiKey is empty"); - } - const label = name.trim(); - if (label.length === 0) { - throw new Error("workspace name is empty"); - } - const url = `${normalized}/api/v1/workspace/new`; - const response = await fetch(url, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - Authorization: `Bearer ${key}`, - }, - body: JSON.stringify({ name: label }), - }); - const text = await response.text(); - if (!response.ok) { - throw new Error(`AnythingLLM API ${response.status}: ${text.slice(0, 500)}`); - } - return parseWorkspaceEnvelope(parseJson(text)); -}; diff --git a/extensions/anythingllm-workspaces/src/anythingllmDocumentApi.ts b/extensions/anythingllm-workspaces/src/anythingllmDocumentApi.ts deleted file mode 100644 index a895cf1..0000000 --- a/extensions/anythingllm-workspaces/src/anythingllmDocumentApi.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as fs from "node:fs/promises"; -import { normalizeAnythingLlmBaseUrl } from "./anythingllmClient"; - -const normalizeApiSecret = (raw: string): string => { - const trimmed = raw.trim(); - const bearerPrefix = /^Bearer\s+/i; - return bearerPrefix.test(trimmed) ? trimmed.replace(bearerPrefix, "").trim() : trimmed; -}; - -const isRecord = (value: unknown): value is Record => - typeof value === "object" && value !== null && !Array.isArray(value); - -export const uploadLocalFileToWorkspace = async ( - baseUrl: string, - apiKey: string, - workspaceSlug: string, - absoluteFilePath: string, - uploadFileName: string, -): Promise => { - const normalized = normalizeAnythingLlmBaseUrl(baseUrl); - const key = normalizeApiSecret(apiKey); - if (key.length === 0) { - throw new Error("anythingllm.apiKey is empty"); - } - const buf = await fs.readFile(absoluteFilePath); - const body = new FormData(); - body.append("file", new Blob([buf]), uploadFileName); - body.append("addToWorkspaces", workspaceSlug); - const url = `${normalized}/api/v1/document/upload`; - const response = await fetch(url, { - method: "POST", - headers: { - Authorization: `Bearer ${key}`, - }, - body, - }); - const text = await response.text(); - let parsed: unknown; - try { - parsed = JSON.parse(text) as unknown; - } catch { - throw new Error(`document upload: non-JSON response ${response.status}: ${text.slice(0, 300)}`); - } - if (!response.ok) { - throw new Error(`document upload ${response.status}: ${text.slice(0, 500)}`); - } - if (!isRecord(parsed)) { - throw new Error("document upload: invalid JSON body"); - } - const success = parsed.success; - const err = parsed.error; - if (success !== true) { - const msg = typeof err === "string" ? err : JSON.stringify(err); - throw new Error(`document upload failed: ${msg}`); - } -}; diff --git a/extensions/anythingllm-workspaces/src/commandParser.ts b/extensions/anythingllm-workspaces/src/commandParser.ts deleted file mode 100644 index 55d1c24..0000000 --- a/extensions/anythingllm-workspaces/src/commandParser.ts +++ /dev/null @@ -1,57 +0,0 @@ -export type ParsedDevCommand = - | { readonly kind: "repos-clone"; readonly url: string; readonly sync: boolean } - | { readonly kind: "repos-list" } - | { readonly kind: "repos-load"; readonly name: string; readonly sync: boolean } - | { readonly kind: "workspace-load"; readonly name: string } - | { readonly kind: "workspace-sync-repo"; readonly name: string } - | { readonly kind: "help" } - | { readonly kind: "unknown"; readonly raw: string }; - -export const parseDevCommandLine = (line: string): ParsedDevCommand => { - const trimmed = line.trim(); - if (trimmed.length === 0) { - return { kind: "unknown", raw: line }; - } - const parts = trimmed.split(/\s+/); - const cmd = parts[0]; - const argRest = parts.slice(1).join(" ").trim(); - if (cmd === "/repos-clone-sync") { - return { kind: "repos-clone", url: argRest, sync: true }; - } - if (cmd === "/repos-clone") { - return { kind: "repos-clone", url: argRest, sync: false }; - } - if (cmd === "repos-list" || cmd === "/repos-list") { - return { kind: "repos-list" }; - } - if (cmd === "/repos-load-sync") { - return { kind: "repos-load", name: argRest, sync: true }; - } - if (cmd === "/repos-load") { - return { kind: "repos-load", name: argRest, sync: false }; - } - if (cmd === "/workspace-load") { - return { kind: "workspace-load", name: argRest }; - } - if (cmd === "/workspace-sync") { - return { kind: "workspace-sync-repo", name: argRest }; - } - if (cmd === "help" || cmd === "/help") { - return { kind: "help" }; - } - return { kind: "unknown", raw: trimmed }; -}; - -export const devCommandsHelpText = (): string => { - return [ - "Commands (one per line):", - " /repos-clone — clone (branch test)", - " /repos-clone-sync — clone + workspace + open folder + optional initial RAG upload (.4nkaiignore)", - " repos-list — list git repos under REPOS_DEVTOOLS_ROOT", - " /repos-load — verify repo + open folder", - " /repos-load-sync — open folder + workspace + optional initial RAG upload", - " /workspace-load — ensure workspace + browser", - " /workspace-sync — ensure workspace + initial RAG upload (repo must exist under root)", - " help — this list", - ].join("\n"); -}; diff --git a/extensions/anythingllm-workspaces/src/config.ts b/extensions/anythingllm-workspaces/src/config.ts deleted file mode 100644 index e8ba8fa..0000000 --- a/extensions/anythingllm-workspaces/src/config.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as vscode from "vscode"; - -const CONFIG_SECTION = "anythingllm"; - -export interface DevToolsConfigSnapshot { - readonly anythingBaseUrl: string; - readonly anythingApiKey: string; - readonly reposApiBaseUrl: string; - readonly reposApiToken: string; - readonly initialSyncAfterClone: boolean; - readonly initialSyncMaxFiles: number; - readonly initialSyncMaxFileBytes: number; -} - -export const readAnythingConfig = (): { baseUrl: string; apiKey: string } => { - const cfg = vscode.workspace.getConfiguration(CONFIG_SECTION); - const baseUrl = typeof cfg.get("baseUrl") === "string" ? (cfg.get("baseUrl") as string) : ""; - const apiKey = typeof cfg.get("apiKey") === "string" ? (cfg.get("apiKey") as string) : ""; - return { baseUrl, apiKey }; -}; - -const readPositiveInt = (cfg: vscode.WorkspaceConfiguration, key: string, fallback: number): number => { - const v = cfg.get(key); - if (typeof v === "number" && Number.isFinite(v) && v > 0) { - return Math.floor(v); - } - return fallback; -}; - -export const readDevToolsConfig = (): DevToolsConfigSnapshot => { - const cfg = vscode.workspace.getConfiguration(CONFIG_SECTION); - const { baseUrl, apiKey } = readAnythingConfig(); - const reposApiBaseUrl = - typeof cfg.get("reposApiBaseUrl") === "string" - ? (cfg.get("reposApiBaseUrl") as string) - : ""; - const reposApiToken = - typeof cfg.get("reposApiToken") === "string" - ? (cfg.get("reposApiToken") as string) - : ""; - const initialSyncAfterClone = cfg.get("initialSyncAfterClone") !== false; - const initialSyncMaxFiles = readPositiveInt(cfg, "initialSyncMaxFiles", 400); - const initialSyncMaxFileBytes = readPositiveInt(cfg, "initialSyncMaxFileBytes", 5_242_880); - return { - anythingBaseUrl: baseUrl, - anythingApiKey: apiKey, - reposApiBaseUrl, - reposApiToken, - initialSyncAfterClone, - initialSyncMaxFiles, - initialSyncMaxFileBytes, - }; -}; diff --git a/extensions/anythingllm-workspaces/src/devToolsExecutor.ts b/extensions/anythingllm-workspaces/src/devToolsExecutor.ts deleted file mode 100644 index 161aaa2..0000000 --- a/extensions/anythingllm-workspaces/src/devToolsExecutor.ts +++ /dev/null @@ -1,249 +0,0 @@ -import type * as vscode from "vscode"; -import { - devCommandsHelpText, - parseDevCommandLine, - type ParsedDevCommand, -} from "./commandParser"; -import { normalizeAnythingLlmBaseUrl } from "./anythingllmClient"; -import { reposApiClone, reposApiList, reposApiLoad } from "./reposApiClient"; -import { ensureWorkspaceForRepoName } from "./workspaceEnsure"; -import { runInitialRagImportFromRepo } from "./initialRagSync"; - -const DEFAULT_BRANCH = "test"; - -const fmt = (value: unknown): string => { - if (typeof value === "string") { - return value; - } - try { - return JSON.stringify(value, null, 2); - } catch { - return String(value); - } -}; - -export interface DevToolsRunnerContext { - readonly anythingBaseUrl: string; - readonly anythingApiKey: string; - readonly reposApiBaseUrl: string; - readonly reposApiToken: string; - readonly initialSyncAfterClone: boolean; - readonly initialSyncMaxFiles: number; - readonly initialSyncMaxFileBytes: number; - readonly default4nkaiignoreTemplateFsPath: string; - readonly openFolder: (fsPath: string) => Thenable; - readonly openAnythingWorkspaceInBrowser: (slug: string) => Thenable; -} - -const assertReposConfig = (ctx: DevToolsRunnerContext): void => { - if (ctx.reposApiBaseUrl.trim().length === 0) { - throw new Error("Set anythingllm.reposApiBaseUrl (repos-devtools-server URL)."); - } - if (ctx.reposApiToken.trim().length === 0) { - throw new Error("Set anythingllm.reposApiToken (same value as REPOS_DEVTOOLS_TOKEN)."); - } -}; - -const assertAnythingConfig = (ctx: DevToolsRunnerContext): void => { - if (ctx.anythingApiKey.trim().length === 0) { - throw new Error("Set anythingllm.apiKey for workspace operations."); - } -}; - -const appendInitialRag = async ( - ctx: DevToolsRunnerContext, - repoRoot: string, - workspaceSlug: string, -): Promise => { - if (!ctx.initialSyncAfterClone) { - return ""; - } - assertAnythingConfig(ctx); - const res = await runInitialRagImportFromRepo({ - baseUrl: ctx.anythingBaseUrl, - apiKey: ctx.anythingApiKey, - workspaceSlug, - repoRoot, - templateFsPath: ctx.default4nkaiignoreTemplateFsPath, - maxFiles: ctx.initialSyncMaxFiles, - maxFileBytes: ctx.initialSyncMaxFileBytes, - }); - return `\n---\nInitial RAG sync: ${fmt(res)}`; -}; - -const runOne = async ( - cmd: ParsedDevCommand, - ctx: DevToolsRunnerContext, -): Promise => { - if (cmd.kind === "help") { - return devCommandsHelpText(); - } - if (cmd.kind === "unknown") { - return `Unknown command: ${cmd.raw}\n${devCommandsHelpText()}`; - } - if (cmd.kind === "repos-list") { - assertReposConfig(ctx); - const data = await reposApiList(ctx.reposApiBaseUrl, ctx.reposApiToken); - return fmt(data); - } - if (cmd.kind === "repos-clone") { - assertReposConfig(ctx); - if (cmd.url.length === 0) { - throw new Error("/repos-clone requires a git URL."); - } - const data = await reposApiClone( - ctx.reposApiBaseUrl, - ctx.reposApiToken, - cmd.url, - DEFAULT_BRANCH, - ); - let out = fmt(data); - if (cmd.sync) { - assertAnythingConfig(ctx); - const rec = data as Record; - const name = rec.name; - const fsPath = rec.path; - if (typeof name !== "string") { - throw new Error("clone response missing name"); - } - if (typeof fsPath !== "string" || fsPath.length === 0) { - throw new Error("clone response missing path"); - } - const ensured = await ensureWorkspaceForRepoName( - ctx.anythingBaseUrl, - ctx.anythingApiKey, - name, - ); - out += `\n---\nAnythingLLM workspace: ${fmt({ - slug: ensured.workspace.slug, - name: ensured.workspace.name, - workspaceCreatedByApi: ensured.created, - })}`; - out += await appendInitialRag(ctx, fsPath, ensured.workspace.slug); - await ctx.openFolder(fsPath); - await ctx.openAnythingWorkspaceInBrowser(ensured.workspace.slug); - } - return out; - } - if (cmd.kind === "repos-load") { - assertReposConfig(ctx); - if (cmd.name.length === 0) { - throw new Error("/repos-load requires a repository folder name."); - } - const loaded = await reposApiLoad( - ctx.reposApiBaseUrl, - ctx.reposApiToken, - cmd.name, - ); - let out = fmt(loaded); - await ctx.openFolder(loaded.path); - if (cmd.sync) { - assertAnythingConfig(ctx); - const ensured = await ensureWorkspaceForRepoName( - ctx.anythingBaseUrl, - ctx.anythingApiKey, - loaded.name, - ); - out += `\n---\nAnythingLLM workspace: ${fmt({ - slug: ensured.workspace.slug, - name: ensured.workspace.name, - workspaceCreatedByApi: ensured.created, - })}`; - out += await appendInitialRag(ctx, loaded.path, ensured.workspace.slug); - await ctx.openAnythingWorkspaceInBrowser(ensured.workspace.slug); - } - return out; - } - if (cmd.kind === "workspace-load") { - assertAnythingConfig(ctx); - if (cmd.name.length === 0) { - throw new Error("/workspace-load requires a workspace name."); - } - const ensured = await ensureWorkspaceForRepoName( - ctx.anythingBaseUrl, - ctx.anythingApiKey, - cmd.name, - ); - await ctx.openAnythingWorkspaceInBrowser(ensured.workspace.slug); - return fmt({ - slug: ensured.workspace.slug, - name: ensured.workspace.name, - workspaceCreatedByApi: ensured.created, - }); - } - if (cmd.kind === "workspace-sync-repo") { - assertReposConfig(ctx); - assertAnythingConfig(ctx); - if (cmd.name.length === 0) { - throw new Error("/workspace-sync requires a repository folder name."); - } - const loaded = await reposApiLoad( - ctx.reposApiBaseUrl, - ctx.reposApiToken, - cmd.name, - ); - const ensured = await ensureWorkspaceForRepoName( - ctx.anythingBaseUrl, - ctx.anythingApiKey, - loaded.name, - ); - let out = fmt({ - repoPath: loaded.path, - slug: ensured.workspace.slug, - name: ensured.workspace.name, - workspaceCreatedByApi: ensured.created, - }); - out += await appendInitialRag(ctx, loaded.path, ensured.workspace.slug); - return out; - } - return `Unhandled: ${JSON.stringify(cmd)}`; -}; - -export const runDevToolsScript = async ( - text: string, - ctx: DevToolsRunnerContext, -): Promise => { - const lines = text - .split("\n") - .map((l) => l.trim()) - .filter((l) => l.length > 0); - if (lines.length === 0) { - return devCommandsHelpText(); - } - const parts: string[] = []; - for (const line of lines) { - const parsed = parseDevCommandLine(line); - try { - parts.push(await runOne(parsed, ctx)); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - parts.push(`ERROR: ${msg}`); - } - } - return parts.join("\n---\n"); -}; - -export const registerDevToolsOpenFolder = ( - vscodeApi: typeof vscode, -): ((fsPath: string) => Thenable) => { - return (fsPath: string) => { - if (fsPath.length === 0) { - return Promise.resolve(); - } - const uri = vscodeApi.Uri.file(fsPath); - return vscodeApi.commands.executeCommand("vscode.openFolder", uri, false); - }; -}; - -export const makeOpenAnythingHandler = ( - vscodeApi: typeof vscode, - baseUrl: string, -): ((slug: string) => Thenable) => { - return (slug: string) => { - const root = normalizeAnythingLlmBaseUrl(baseUrl); - const uri = vscodeApi.Uri.parse( - `${root}/workspace/${encodeURIComponent(slug)}`, - ); - return vscodeApi.env.openExternal(uri).then(() => undefined); - }; -}; diff --git a/extensions/anythingllm-workspaces/src/devToolsPanel.ts b/extensions/anythingllm-workspaces/src/devToolsPanel.ts deleted file mode 100644 index 13be113..0000000 --- a/extensions/anythingllm-workspaces/src/devToolsPanel.ts +++ /dev/null @@ -1,112 +0,0 @@ -import * as vscode from "vscode"; -import type { DevToolsConfigSnapshot } from "./config"; -import { runDevToolsScript, makeOpenAnythingHandler, registerDevToolsOpenFolder } from "./devToolsExecutor"; - -const PANEL_ID = "anythingllmDevTools"; - -const panelTitle = "AnythingLLM dev tools"; - -const buildHtml = ( - webview: vscode.Webview, - extensionUri: vscode.Uri, -): string => { - const scriptUri = webview.asWebviewUri( - vscode.Uri.joinPath(extensionUri, "media", "devTools.js"), - ); - const csp = webview.cspSource; - return ` - - - - - - ${panelTitle} - - - -
- - -
-
- - -
- -

-  
-
-`;
-};
-
-export const showDevToolsPanel = (
-  context: vscode.ExtensionContext,
-  readConfig: () => DevToolsConfigSnapshot,
-): void => {
-  const column = vscode.ViewColumn.Beside;
-  const panel = vscode.window.createWebviewPanel(
-    PANEL_ID,
-    panelTitle,
-    column,
-    {
-      enableScripts: true,
-      retainContextWhenHidden: true,
-      localResourceRoots: [
-        vscode.Uri.joinPath(context.extensionUri, "media"),
-      ],
-    },
-  );
-
-  panel.webview.html = buildHtml(panel.webview, context.extensionUri);
-
-  const openFolder = registerDevToolsOpenFolder(vscode);
-  const templateFsPath = vscode.Uri.joinPath(
-    context.extensionUri,
-    "templates",
-    "4nkaiignore.default",
-  ).fsPath;
-
-  panel.webview.onDidReceiveMessage(
-    (msg: unknown) => {
-      void (async () => {
-        if (typeof msg !== "object" || msg === null) {
-          return;
-        }
-        const rec = msg as Record;
-        if (rec.type !== "run") {
-          return;
-        }
-        const text = typeof rec.text === "string" ? rec.text : "";
-        const use = readConfig();
-        const openBrowser = makeOpenAnythingHandler(vscode, use.anythingBaseUrl);
-        try {
-          const result = await runDevToolsScript(text, {
-            anythingBaseUrl: use.anythingBaseUrl,
-            anythingApiKey: use.anythingApiKey,
-            reposApiBaseUrl: use.reposApiBaseUrl,
-            reposApiToken: use.reposApiToken,
-            initialSyncAfterClone: use.initialSyncAfterClone,
-            initialSyncMaxFiles: use.initialSyncMaxFiles,
-            initialSyncMaxFileBytes: use.initialSyncMaxFileBytes,
-            default4nkaiignoreTemplateFsPath: templateFsPath,
-            openFolder,
-            openAnythingWorkspaceInBrowser: openBrowser,
-          });
-          panel.webview.postMessage({ type: "result", text: result });
-        } catch (e) {
-          const m = e instanceof Error ? e.message : String(e);
-          panel.webview.postMessage({ type: "result", text: `ERROR: ${m}` });
-        }
-      })();
-    },
-    undefined,
-    context.subscriptions,
-  );
-};
diff --git a/extensions/anythingllm-workspaces/src/extension.ts b/extensions/anythingllm-workspaces/src/extension.ts
deleted file mode 100644
index 3efe908..0000000
--- a/extensions/anythingllm-workspaces/src/extension.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import * as vscode from "vscode";
-import { readAnythingConfig, readDevToolsConfig } from "./config";
-import { listWorkspaces, normalizeAnythingLlmBaseUrl } from "./anythingllmClient";
-import type { AnythingWorkspace } from "./types";
-import { showDevToolsPanel } from "./devToolsPanel";
-
-const workspaceLabel = (w: AnythingWorkspace): string => `${w.name} (${w.slug})`;
-
-const openWorkspaceInBrowser = async (baseUrl: string, slug: string): Promise => {
-  const root = normalizeAnythingLlmBaseUrl(baseUrl);
-  const path = `/workspace/${encodeURIComponent(slug)}`;
-  const uri = vscode.Uri.parse(`${root}${path}`);
-  await vscode.env.openExternal(uri);
-};
-
-export const activate = (context: vscode.ExtensionContext): void => {
-  const listCmd = vscode.commands.registerCommand("anythingllm.listWorkspaces", async () => {
-    const { baseUrl, apiKey } = readAnythingConfig();
-    try {
-      const workspaces = await listWorkspaces(baseUrl, apiKey);
-      if (workspaces.length === 0) {
-        void vscode.window.showInformationMessage("AnythingLLM: no workspaces.");
-        return;
-      }
-      const picked = await vscode.window.showQuickPick(
-        workspaces.map((w) => ({
-          label: workspaceLabel(w),
-          workspace: w,
-        })),
-        { placeHolder: "Select a workspace" },
-      );
-      if (picked === undefined) {
-        return;
-      }
-      await openWorkspaceInBrowser(baseUrl, picked.workspace.slug);
-    } catch (e) {
-      const message = e instanceof Error ? e.message : String(e);
-      void vscode.window.showErrorMessage(`AnythingLLM: ${message}`);
-    }
-  });
-
-  const openUiCmd = vscode.commands.registerCommand("anythingllm.openWebUi", async () => {
-    const { baseUrl } = readAnythingConfig();
-    try {
-      const root = normalizeAnythingLlmBaseUrl(baseUrl);
-      await vscode.env.openExternal(vscode.Uri.parse(root));
-    } catch (e) {
-      const message = e instanceof Error ? e.message : String(e);
-      void vscode.window.showErrorMessage(`AnythingLLM: ${message}`);
-    }
-  });
-
-  const devPanelCmd = vscode.commands.registerCommand("anythingllm.openDevToolsPanel", () => {
-    showDevToolsPanel(context, readDevToolsConfig);
-  });
-
-  context.subscriptions.push(listCmd, openUiCmd, devPanelCmd);
-};
-
-export const deactivate = (): void => {
-  /* no-op */
-};
diff --git a/extensions/anythingllm-workspaces/src/initialRagSync.ts b/extensions/anythingllm-workspaces/src/initialRagSync.ts
deleted file mode 100644
index 675b6cc..0000000
--- a/extensions/anythingllm-workspaces/src/initialRagSync.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-import ignore from "ignore";
-import * as fs from "node:fs/promises";
-import * as path from "node:path";
-import { uploadLocalFileToWorkspace } from "./anythingllmDocumentApi";
-
-const ALWAYS_IGNORE = [".git/", "node_modules/", "**/node_modules/"].join("\n");
-
-export interface InitialRagImportResult {
-  readonly uploaded: number;
-  readonly skipped: number;
-  readonly errors: readonly string[];
-  readonly dotfileCreated: boolean;
-  readonly capped: boolean;
-}
-
-export const ensureDot4nkaiignoreFromTemplate = async (
-  repoRoot: string,
-  templateFsPath: string,
-): Promise<{ created: boolean }> => {
-  const target = path.join(repoRoot, ".4nkaiignore");
-  try {
-    await fs.access(target);
-    return { created: false };
-  } catch {
-    const tmpl = await fs.readFile(templateFsPath, "utf8");
-    await fs.writeFile(target, tmpl, "utf8");
-    return { created: true };
-  }
-};
-
-const walkFiles = async (dir: string): Promise => {
-  const out: string[] = [];
-  const scan = async (d: string): Promise => {
-    const entries = await fs.readdir(d, { withFileTypes: true });
-    for (const e of entries) {
-      const p = path.join(d, e.name);
-      if (e.isSymbolicLink()) {
-        continue;
-      }
-      if (e.isDirectory()) {
-        await scan(p);
-        continue;
-      }
-      if (e.isFile()) {
-        out.push(p);
-      }
-    }
-  };
-  await scan(dir);
-  return out;
-};
-
-const toPosixRel = (root: string, abs: string): string => {
-  const rel = path.relative(root, abs);
-  return rel.split(path.sep).join("/");
-};
-
-const uploadNameForRel = (rel: string): string => {
-  return rel.split("/").join("__");
-};
-
-export const runInitialRagImportFromRepo = async (opts: {
-  readonly baseUrl: string;
-  readonly apiKey: string;
-  readonly workspaceSlug: string;
-  readonly repoRoot: string;
-  readonly templateFsPath: string;
-  readonly maxFiles: number;
-  readonly maxFileBytes: number;
-}): Promise => {
-  const dot = await ensureDot4nkaiignoreFromTemplate(opts.repoRoot, opts.templateFsPath);
-  const ignorePath = path.join(opts.repoRoot, ".4nkaiignore");
-  let userRules = "";
-  try {
-    userRules = await fs.readFile(ignorePath, "utf8");
-  } catch {
-    userRules = "";
-  }
-  const ig = ignore();
-  ig.add(ALWAYS_IGNORE);
-  ig.add(userRules);
-
-  const absFiles = await walkFiles(opts.repoRoot);
-  const candidates: string[] = [];
-  for (const abs of absFiles) {
-    const rel = toPosixRel(opts.repoRoot, abs);
-    if (rel.length === 0 || rel.startsWith("..")) {
-      continue;
-    }
-    if (ig.ignores(rel)) {
-      continue;
-    }
-    candidates.push(abs);
-  }
-
-  let uploaded = 0;
-  let skipped = 0;
-  const errors: string[] = [];
-  let capped = false;
-
-  for (const abs of candidates) {
-    if (uploaded >= opts.maxFiles) {
-      capped = true;
-      skipped += 1;
-      continue;
-    }
-    const st = await fs.stat(abs);
-    if (st.size > opts.maxFileBytes) {
-      skipped += 1;
-      continue;
-    }
-    const rel = toPosixRel(opts.repoRoot, abs);
-    const uploadName = uploadNameForRel(rel);
-    try {
-      await uploadLocalFileToWorkspace(
-        opts.baseUrl,
-        opts.apiKey,
-        opts.workspaceSlug,
-        abs,
-        uploadName,
-      );
-      uploaded += 1;
-    } catch (e) {
-      const m = e instanceof Error ? e.message : String(e);
-      errors.push(`${rel}: ${m}`);
-    }
-  }
-
-  return {
-    uploaded,
-    skipped,
-    errors,
-    dotfileCreated: dot.created,
-    capped,
-  };
-};
diff --git a/extensions/anythingllm-workspaces/src/reposApiClient.ts b/extensions/anythingllm-workspaces/src/reposApiClient.ts
deleted file mode 100644
index fa6e78e..0000000
--- a/extensions/anythingllm-workspaces/src/reposApiClient.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-const trimSlash = (u: string): string => u.replace(/\/+$/, "");
-
-const normalizeReposToken = (raw: string): string => {
-  const trimmed = raw.trim();
-  const bearerPrefix = /^Bearer\s+/i;
-  return bearerPrefix.test(trimmed) ? trimmed.replace(bearerPrefix, "").trim() : trimmed;
-};
-
-const authHeaders = (token: string): Record => {
-  const key = normalizeReposToken(token);
-  return {
-    Accept: "application/json",
-    "Content-Type": "application/json",
-    Authorization: `Bearer ${key}`,
-  };
-};
-
-export const reposApiClone = async (
-  baseUrl: string,
-  token: string,
-  url: string,
-  branch: string,
-): Promise => {
-  const root = trimSlash(baseUrl.trim());
-  if (root.length === 0) {
-    throw new Error("anythingllm.reposApiBaseUrl is empty");
-  }
-  const res = await fetch(`${root}/repos-clone`, {
-    method: "POST",
-    headers: authHeaders(token),
-    body: JSON.stringify({ url, branch }),
-  });
-  const text = await res.text();
-  let body: unknown = text;
-  try {
-    body = JSON.parse(text) as unknown;
-  } catch {
-    /* keep text */
-  }
-  if (!res.ok) {
-    throw new Error(
-      `repos API ${res.status}: ${typeof body === "string" ? body : JSON.stringify(body)}`,
-    );
-  }
-  return body;
-};
-
-export const reposApiList = async (baseUrl: string, token: string): Promise => {
-  const root = trimSlash(baseUrl.trim());
-  if (root.length === 0) {
-    throw new Error("anythingllm.reposApiBaseUrl is empty");
-  }
-  const res = await fetch(`${root}/repos-list`, {
-    method: "GET",
-    headers: {
-      Accept: "application/json",
-      Authorization: `Bearer ${normalizeReposToken(token)}`,
-    },
-  });
-  const text = await res.text();
-  if (!res.ok) {
-    throw new Error(`repos API ${res.status}: ${text}`);
-  }
-  return JSON.parse(text) as unknown;
-};
-
-export const reposApiLoad = async (
-  baseUrl: string,
-  token: string,
-  name: string,
-): Promise<{ path: string; name: string }> => {
-  const root = trimSlash(baseUrl.trim());
-  if (root.length === 0) {
-    throw new Error("anythingllm.reposApiBaseUrl is empty");
-  }
-  const res = await fetch(`${root}/repos-load`, {
-    method: "POST",
-    headers: authHeaders(token),
-    body: JSON.stringify({ name }),
-  });
-  const text = await res.text();
-  const body = JSON.parse(text) as unknown;
-  if (!res.ok) {
-    throw new Error(`repos API ${res.status}: ${text}`);
-  }
-  if (typeof body !== "object" || body === null) {
-    throw new Error("repos-load: invalid response");
-  }
-  const rec = body as Record;
-  const p = rec.path;
-  const n = rec.name;
-  if (typeof p !== "string" || typeof n !== "string") {
-    throw new Error("repos-load: missing path or name");
-  }
-  return { path: p, name: n };
-};
diff --git a/extensions/anythingllm-workspaces/src/types.ts b/extensions/anythingllm-workspaces/src/types.ts
deleted file mode 100644
index 08ffa7d..0000000
--- a/extensions/anythingllm-workspaces/src/types.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export interface AnythingWorkspace {
-  readonly id: number;
-  readonly name: string;
-  readonly slug: string;
-  readonly createdAt?: string;
-  readonly lastUpdatedAt?: string;
-}
diff --git a/extensions/anythingllm-workspaces/src/workspaceEnsure.ts b/extensions/anythingllm-workspaces/src/workspaceEnsure.ts
deleted file mode 100644
index 51b732c..0000000
--- a/extensions/anythingllm-workspaces/src/workspaceEnsure.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { createWorkspace, listWorkspaces } from "./anythingllmClient";
-import type { AnythingWorkspace } from "./types";
-
-export interface EnsuredWorkspace {
-  readonly workspace: AnythingWorkspace;
-  readonly created: boolean;
-}
-
-export const ensureWorkspaceForRepoName = async (
-  baseUrl: string,
-  apiKey: string,
-  repoName: string,
-): Promise => {
-  const key = repoName.trim();
-  if (key.length === 0) {
-    throw new Error("repo/workspace name is empty");
-  }
-  const all = await listWorkspaces(baseUrl, apiKey);
-  const byName = all.find((w) => w.name === key);
-  const bySlug = all.find((w) => w.slug === key);
-  const found = byName ?? bySlug;
-  if (found) {
-    return { workspace: found, created: false };
-  }
-  const created = await createWorkspace(baseUrl, apiKey, key);
-  return { workspace: created, created: true };
-};
diff --git a/extensions/anythingllm-workspaces/templates/4nkaiignore.default b/extensions/anythingllm-workspaces/templates/4nkaiignore.default
deleted file mode 100644
index be598ec..0000000
--- a/extensions/anythingllm-workspaces/templates/4nkaiignore.default
+++ /dev/null
@@ -1,54 +0,0 @@
-# .4nkaiignore — same rules as .gitignore (see gitignore(5))
-# Used by anythingllm-devtools and repos-devtools-server (post-clone) to filter uploads
-# and by the legacy VS Code extension after /repos-load-sync. Copy or rename to `.4nkaiignore`.
-
-# VCS
-.git/
-
-# Dependencies & build outputs
-node_modules/
-**/node_modules/
-dist/
-out/
-build/
-.next/
-.turbo/
-coverage/
-.nyc_output/
-target/
-
-# IDE / OS
-.idea/
-.vscode/
-.DS_Store
-Thumbs.db
-
-# Secrets & local env (never embed)
-.env
-.env.*
-!.env.example
-
-# Large or binary artifacts (remove a line if your project should embed that type)
-*.png
-*.jpg
-*.jpeg
-*.gif
-*.webp
-*.ico
-*.pdf
-*.zip
-*.tar
-*.gz
-*.7z
-*.wasm
-*.so
-*.dylib
-*.dll
-*.exe
-*.mp4
-*.mp3
-
-# Minified bundles (often redundant with sources)
-*.min.js
-*.min.css
-*.map
diff --git a/extensions/anythingllm-workspaces/tsconfig.json b/extensions/anythingllm-workspaces/tsconfig.json
deleted file mode 100644
index 663c13e..0000000
--- a/extensions/anythingllm-workspaces/tsconfig.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-  "compilerOptions": {
-    "module": "commonjs",
-    "target": "ES2022",
-    "lib": ["ES2022"],
-    "outDir": "out",
-    "rootDir": "src",
-    "strict": true,
-    "esModuleInterop": true,
-    "skipLibCheck": true,
-    "forceConsistentCasingInFileNames": true,
-    "noImplicitReturns": true,
-    "noUnusedLocals": true,
-    "noUnusedParameters": true
-  },
-  "exclude": ["node_modules", "out"]
-}
diff --git a/projects/example/conf.json b/projects/example/conf.json
index fd9acc2..be314c1 100644
--- a/projects/example/conf.json
+++ b/projects/example/conf.json
@@ -56,6 +56,9 @@
 			"settings": {
 				"smartIde.activeProjectId": ""
 			}
+		},
+		"preview_urls": {
+			"test": "https://.test.example/"
 		}
 	}
 }
diff --git a/scripts/open-carbonyl-preview-test.sh b/scripts/open-carbonyl-preview-test.sh
new file mode 100755
index 0000000..2ba3c15
--- /dev/null
+++ b/scripts/open-carbonyl-preview-test.sh
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+# Open smart_ide.preview_urls.test for the active project in Carbonyl (terminal browser).
+set -euo pipefail
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+PROJECT_ID=""
+
+while [[ $# -gt 0 ]]; do
+	case "$1" in
+		--project)
+			PROJECT_ID="${2:?}"
+			shift 2
+			;;
+		-h | --help)
+			echo "usage: $(basename "$0") [--project ]" >&2
+			echo "  Resolves project id: --project, SMART_IDE_PROJECT_ID, or projects/active-project.json" >&2
+			exit 0
+			;;
+		*)
+			echo "unknown arg: $1" >&2
+			exit 1
+			;;
+	esac
+done
+
+if [[ -z "${PROJECT_ID}" ]]; then
+	if [[ -n "${SMART_IDE_PROJECT_ID:-}" ]]; then
+		PROJECT_ID="${SMART_IDE_PROJECT_ID}"
+	fi
+fi
+
+ACTIVE="${ROOT}/projects/active-project.json"
+if [[ -z "${PROJECT_ID}" && -f "${ACTIVE}" ]]; then
+	PROJECT_ID="$(python3 -c "
+import json
+with open('${ACTIVE}', encoding='utf-8') as f:
+    d = json.load(f)
+print(d.get('id') or '')
+" 2>/dev/null || true)"
+fi
+
+if [[ -z "${PROJECT_ID}" ]]; then
+	echo "Set SMART_IDE_PROJECT_ID, copy projects/active-project.json.example to projects/active-project.json, or pass --project " >&2
+	exit 1
+fi
+
+CONF="${ROOT}/projects/${PROJECT_ID}/conf.json"
+if [[ ! -f "${CONF}" ]]; then
+	echo "Missing ${CONF}" >&2
+	exit 1
+fi
+
+URL="$(python3 -c "
+import json
+import sys
+with open('${CONF}', encoding='utf-8') as f:
+    d = json.load(f)
+si = d.get('smart_ide') or {}
+pu = si.get('preview_urls') or {}
+url = (pu.get('test') or '').strip()
+if url:
+    print(url)
+    sys.exit(0)
+sys.exit(1)
+" 2>/dev/null || true)"
+
+if [[ -z "${URL}" ]]; then
+	if [[ -n "${PREVIEW_TEST_URL:-}" ]]; then
+		URL="${PREVIEW_TEST_URL}"
+	else
+		echo "No preview URL: set smart_ide.preview_urls.test in ${CONF} or export PREVIEW_TEST_URL" >&2
+		exit 1
+	fi
+fi
+
+exec "${ROOT}/services/carbonyl/run-carbonyl.sh" "${URL}"
diff --git a/services/anythingllm-devtools/README.md b/services/anythingllm-devtools/README.md
index b4c28bd..f1267e1 100644
--- a/services/anythingllm-devtools/README.md
+++ b/services/anythingllm-devtools/README.md
@@ -1,6 +1,6 @@
 # anythingllm-devtools
 
-Service HTTP local : orchestration **repos-devtools-server** + API **AnythingLLM** (workspaces, upload documents initiaux selon `.4nkaiignore`). Remplace l’usage principal de l’extension VS Code / Cursor `extensions/anythingllm-workspaces` (voir [extension-anythingllm-workspaces.md](../../docs/repo/extension-anythingllm-workspaces.md)).
+Service HTTP local : orchestration **repos-devtools-server** + API **AnythingLLM** (workspaces, upload documents initiaux selon `.4nkaiignore`). Remplace l’ancienne extension IDE AnythingLLM (voir [extension-anythingllm-workspaces.md](../../docs/repo/extension-anythingllm-workspaces.md)).
 
 ## Prérequis
 
diff --git a/services/carbonyl/README.md b/services/carbonyl/README.md
new file mode 100644
index 0000000..c4acc07
--- /dev/null
+++ b/services/carbonyl/README.md
@@ -0,0 +1,45 @@
+# carbonyl — navigateur terminal (amont)
+
+[Carbonyl](https://github.com/fathyb/carbonyl) est un navigateur basé sur Chromium affiché dans le terminal (Web APIs, médias, utilisable sans serveur graphique, y compris via SSH).
+
+Ce répertoire **`services/carbonyl/`** regroupe :
+
+- **`upstream/`** : sous-module Git pointant vers le dépôt amont **fathyb/carbonyl** (fork / suivi des évolutions amont dans ce monorepo).
+- **`run-carbonyl.sh`** : lancement via **Docker** (image publique `fathyb/carbonyl`) ou binaire **`carbonyl`** si installé (`npm install -g carbonyl`).
+
+Ne pas exécuter **`git submodule update --init --recursive`** dans **`upstream/`** tant qu’un build Chromium complet n’est pas requis : le sous-module **chromium** amont est très volumineux. Pour un usage quotidien, préférer Docker ou le binaire précompilé.
+
+## Prérequis
+
+- **Docker** (recommandé) : `docker run --rm -ti fathyb/carbonyl https://example.com`
+- ou dépendances **Chromium** sur l’hôte si build / binaire local — voir le [readme amont](https://github.com/fathyb/carbonyl/blob/main/readme.md).
+
+## Variables (optionnelles)
+
+| Variable | Rôle |
+|----------|------|
+| `CARBONYL_DOCKER_IMAGE` | Image Docker (défaut `fathyb/carbonyl`) |
+| `CARBONYL_RUNNER` | `docker` (défaut) ou `native` pour appeler `carbonyl` dans le `PATH` |
+
+## Scripts
+
+- **`./run-carbonyl.sh `** — ouvre l’URL dans Carbonyl.
+- Depuis la racine du monorepo : **`scripts/open-carbonyl-preview-test.sh`** — ouvre l’URL **test** déclarée pour le projet actif (voir ci-dessous).
+
+## Prévisualisation des déploiements **test**
+
+Dans **`projects//conf.json`**, sous **`smart_ide`**, champ optionnel **`preview_urls`** :
+
+```json
+"preview_urls": {
+	"test": "https://app.example.test/"
+}
+```
+
+Le script **`scripts/open-carbonyl-preview-test.sh`** lit **`projects/active-project.json`** (ou **`SMART_IDE_PROJECT_ID`**, ou argument **`--project `**) puis **`preview_urls.test`**. En secours : variable **`PREVIEW_TEST_URL`**.
+
+Documentation : [docs/repo/service-carbonyl.md](../../docs/repo/service-carbonyl.md), [docs/features/carbonyl-terminal-browser.md](../../docs/features/carbonyl-terminal-browser.md).
+
+## Licence amont
+
+Carbonyl est sous licence **BSD-3-Clause** (voir `upstream/license.md`).
diff --git a/services/carbonyl/run-carbonyl.sh b/services/carbonyl/run-carbonyl.sh
new file mode 100755
index 0000000..3a2d0bb
--- /dev/null
+++ b/services/carbonyl/run-carbonyl.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+# Launch Carbonyl (terminal browser) for a URL. Prefer Docker image fathyb/carbonyl.
+set -euo pipefail
+URL="${1:?usage: $(basename "$0") }"
+
+RUNNER="${CARBONYL_RUNNER:-docker}"
+IMAGE="${CARBONYL_DOCKER_IMAGE:-fathyb/carbonyl}"
+
+if [[ "${RUNNER}" == "native" ]]; then
+	if ! command -v carbonyl >/dev/null 2>&1; then
+		echo "carbonyl not in PATH; install e.g. npm install -g carbonyl or set CARBONYL_RUNNER=docker" >&2
+		exit 1
+	fi
+	exec carbonyl "${URL}"
+fi
+
+if ! command -v docker >/dev/null 2>&1; then
+	echo "docker not found; install Docker or set CARBONYL_RUNNER=native with carbonyl in PATH" >&2
+	exit 1
+fi
+
+exec docker run --rm -ti "${IMAGE}" "${URL}"
diff --git a/services/carbonyl/upstream b/services/carbonyl/upstream
new file mode 160000
index 0000000..ab80a27
--- /dev/null
+++ b/services/carbonyl/upstream
@@ -0,0 +1 @@
+Subproject commit ab80a276b1bd1c2c8dcefc8f248415dfc61dc2bf