Remove AnythingLLM VS Code extension; add Carbonyl service (upstream submodule)

- Delete extensions/anythingllm-workspaces; document migration to anythingllm-devtools
- Add services/carbonyl: shallow submodule fathyb/carbonyl, run-carbonyl.sh (Docker/native)
- Add scripts/open-carbonyl-preview-test.sh and smart_ide.preview_urls.test in example conf
- Docs: service-carbonyl, carbonyl-terminal-browser, architecture index updates
This commit is contained in:
4NK 2026-04-03 22:13:34 +02:00 committed by Nicolas Cantu
parent 524d38b9ff
commit 3f1894e21f
40 changed files with 223 additions and 1304 deletions

4
.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule "services/carbonyl/upstream"]
path = services/carbonyl/upstream
url = https://github.com/fathyb/carbonyl.git
shallow = true

View File

@ -62,5 +62,5 @@ Erreurs `400` : corps JSON invalide ou message derreur métier (ex. repo intr
## Voir aussi ## Voir aussi
- [anythingllm-workspaces.md](../anythingllm-workspaces.md) - [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) - [repos-devtools-server.md](./repos-devtools-server.md)

View File

@ -23,7 +23,8 @@ Vue densemble 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/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/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/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. 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 | | Document | Contenu |
|----------|---------| |----------|---------|
| [anythingllm-workspaces.md](./anythingllm-workspaces.md) | Un workspace AnythingLLM par projet, synchronisation | | [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 | | [ux-navigation-model.md](./ux-navigation-model.md) | Intentions, recherche, mode expert |
## Intégration dépôts ## 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/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/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/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/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-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/<id>/data`, `DOCV_PROJECTS_ROOT`, multi-hôte | | [features/docv-service-integration.md](./features/docv-service-integration.md) | docv gestion documentaire, `../projects/<id>/data`, `DOCV_PROJECTS_ROOT`, multi-hôte |

View File

@ -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 dun 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/<id>/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 lusage courant : **Docker** (`fathyb/carbonyl`) ou paquet **`carbonyl`** npm.
## Documentation liée
- [repo/service-carbonyl.md](../repo/service-carbonyl.md)

View File

@ -9,7 +9,7 @@
## Comportement ## Comportement
1. **Serveur `repos-devtools-server`** : après `git clone` réussi, copie **`templates/4nkaiignore.default`** vers **`<repo>/.4nkaiignore`** si absent. 1. **Serveur `repos-devtools-server`** : après `git clone` réussi, copie **`templates/4nkaiignore.default`** vers **`<repo>/.4nkaiignore`** si absent.
2. **Service `anythingllm-devtools`** (ou extension héritée) : après `/repos-clone-sync`, `/repos-load-sync`, ou sur **`/workspace-sync <nom>`**, si la synchro initiale nest 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 <nom>`**, si la synchro initiale nest pas désactivée (`ANYTHINGLLM_INITIAL_SYNC_AFTER_CLONE` côté service) :
- assure **`.4nkaiignore`** depuis le template si toujours absent ; - assure **`.4nkaiignore`** depuis le template si toujours absent ;
- parcourt le dépôt, applique règles de base + `.4nkaiignore` ; - 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. - 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/anythingllm-devtools/templates/4nkaiignore.default`** (référence)
- **`services/repos-devtools-server/templates/4nkaiignore.default`** (même contenu ; à maintenir en parité) - **`services/repos-devtools-server/templates/4nkaiignore.default`** (même contenu ; à maintenir en parité)
- **`extensions/anythingllm-workspaces/templates/4nkaiignore.default`** (copie héritée)
Lutilisateur renomme / copie en **`.4nkaiignore`** à la racine du projet et adapte les règles. Lutilisateur 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 linstanc
## Modalités de déploiement ## Modalités de déploiement
- Rebuild et redémarrage de **repos-devtools-server** et de **anythingllm-devtools** ; repackaging / réinstallation de lextension uniquement si vous conservez encore la surface IDE. - Rebuild et redémarrage de **repos-devtools-server** et de **anythingllm-devtools**.

View File

@ -1,6 +1,6 @@
# Portage AnythingLLM Workspaces → Lapce (`core_ide/`) # Portage AnythingLLM Workspaces → Lapce (`core_ide/`)
Lorchestration AnythingLLM + repos-devtools est exposée en **service HTTP** [`services/anythingllm-devtools/`](../../services/anythingllm-devtools/) ; lextension [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). Lorchestration AnythingLLM + repos-devtools est exposée en **service HTTP** [`services/anythingllm-devtools/`](../../services/anythingllm-devtools/). Lancienne 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 ## Phase 1 — Connectivité sans webview
@ -13,7 +13,7 @@ Lorchestration AnythingLLM + repos-devtools est exposée en **service HTTP**
## Phase 2 — Parité « Dev tools » et sync RAG ## 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). - 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). - Ouvrir le dossier dépôt dans Lapce après clone (API workspace Lapce).

View File

@ -37,6 +37,7 @@ Toute la documentation **opérationnelle** qui vivait auparavant sous des `READM
| [service-langextract.md](./service-langextract.md) | Wrapper LangExtract | | [service-langextract.md](./service-langextract.md) | Wrapper LangExtract |
| **Scripts et extensions** | | | **Scripts et extensions** | |
| [script-anythingllm-pull-sync.md](./script-anythingllm-pull-sync.md) | Hook post-merge → upload AnythingLLM | | [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/). Les **spécifications** détaillées (contrats HTTP, sécurité, orchestration) restent dans [../API/README.md](../API/README.md) et [../features/](../features/).

View File

@ -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`) ; lorchestrateur, les agents et les scripts appellent ce service plutôt que lIDE. Lextension 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. Toute lorchestration 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).
## Prérequis
- AnythingLLM avec **clé API développeur** (ne pas confondre avec le Bearer nginx pour `/ollama/`).
- **repos-devtools-server** sur lhô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 lextension 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).

View File

@ -38,7 +38,7 @@ Les agents ne modifient pas `projects/<id>/conf.json` sans validation humaine ex
| `deploy.host_stays_on_test` | no | Comportement `deploy-by-script-to.sh` (branche test vs pprod/prod). | | `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. | | `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é. | | `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`. Détail ticketing : `ia_dev/projects/ia_dev/docs/TICKETS_SPOOL_FORMAT.md`.

View File

@ -48,7 +48,7 @@ Spécification : [features/remote-deployed-data-ssh.md](../features/remote-deplo
### Bloc optionnel `smart_ide` dans `conf.json` ### 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 ### Projet actif pour léditeur / Cursor

View File

@ -48,4 +48,4 @@ rm -f /path/vers/repo/.git/hooks/post-merge
## Liens ## 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).

View File

@ -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** dURLs dans un terminal (SSH sans GUI, session locale).
- **Prévisualisation** des applications déployées en **test** : URL optionnelle dans **`projects/<id>/conf.json`** → **`smart_ide.preview_urls.test`**, ouverte par **`scripts/open-carbonyl-preview-test.sh`**.
Ce nest **pas** un service HTTP : pas de port découte dans smart_ide. Lexé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).

View File

@ -33,7 +33,7 @@ Unité systemd utilisateur possible : `systemctl --user daemon-reload && systemc
## Templates ## 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 ## Spécification HTTP

View File

@ -34,7 +34,7 @@ Ce document décrit les **services logiciels** typiques sur l**hôte** (serve
## Micro-services HTTP sous `services/` ## Micro-services HTTP sous `services/`
Services dappoint 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 lindex dexploitation [repo/README.md](./repo/README.md) (fichiers `repo/service-*.md`). Services dappoint 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 lindex dexploitation [repo/README.md](./repo/README.md) (fichiers `repo/service-*.md`). **Carbonyl** (`services/carbonyl/`) nest 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 ## Documentation liée

View File

@ -13,7 +13,7 @@ Vue produit multi-environnements, SSO et option navigateur : [platform-target.md
## Monorepo unique ## 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 : 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) | | `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 | | `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) | | `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 dexploitation, unités utilisateur pour services | | `scripts/` , `setup/` , `systemd/` | Installation hôte, scripts dexploitation, unités utilisateur pour services |
| `cron/` | Pull **Git** planifié des clones décrits par `projects/<id>/conf.json` (`project_path`) — [repo/cron-git-pull.md](./repo/cron-git-pull.md) | | `cron/` | Pull **Git** planifié des clones décrits par `projects/<id>/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 | | `services/local-office/` | **API REST** Office (upload, commandes docx, stockage SQLite + fichiers) ; complément programmatique à ONLYOFFICE |

View File

@ -1,3 +0,0 @@
node_modules/
out/
*.vsix

View File

@ -1,5 +0,0 @@
.vscode/**
src/**
tsconfig.json
.gitignore
**/*.map

View File

@ -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)**

View File

@ -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);
}
});
})();

View File

@ -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
}
}
}

View File

@ -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"
}
}

View File

@ -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<string, unknown> =>
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<readonly AnythingWorkspace[]> => {
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<AnythingWorkspace> => {
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));
};

View File

@ -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<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
export const uploadLocalFileToWorkspace = async (
baseUrl: string,
apiKey: string,
workspaceSlug: string,
absoluteFilePath: string,
uploadFileName: string,
): Promise<void> => {
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}`);
}
};

View File

@ -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 <git-url> — clone (branch test)",
" /repos-clone-sync <url> — clone + workspace + open folder + optional initial RAG upload (.4nkaiignore)",
" repos-list — list git repos under REPOS_DEVTOOLS_ROOT",
" /repos-load <name> — verify repo + open folder",
" /repos-load-sync <name> — open folder + workspace + optional initial RAG upload",
" /workspace-load <name> — ensure workspace + browser",
" /workspace-sync <name> — ensure workspace + initial RAG upload (repo must exist under root)",
" help — this list",
].join("\n");
};

View File

@ -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,
};
};

View File

@ -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<void>;
readonly openAnythingWorkspaceInBrowser: (slug: string) => Thenable<void>;
}
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<string> => {
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<string> => {
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<string, unknown>;
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<string> => {
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<void>) => {
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<void>) => {
return (slug: string) => {
const root = normalizeAnythingLlmBaseUrl(baseUrl);
const uri = vscodeApi.Uri.parse(
`${root}/workspace/${encodeURIComponent(slug)}`,
);
return vscodeApi.env.openExternal(uri).then(() => undefined);
};
};

View File

@ -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 `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${csp} 'unsafe-inline'; script-src ${csp};" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${panelTitle}</title>
<style>
body { font-family: system-ui, sans-serif; margin: 0.75rem; color: var(--vscode-foreground); background: var(--vscode-editor-background); }
label { display: block; margin-bottom: 0.35rem; font-weight: 600; }
textarea#cmd { width: 100%; min-height: 7rem; box-sizing: border-box; font-family: monospace; font-size: 12px; }
pre#out { white-space: pre-wrap; word-break: break-word; min-height: 4rem; padding: 0.5rem; border: 1px solid var(--vscode-panel-border); }
.row { margin-bottom: 0.5rem; }
button { margin-right: 0.5rem; }
</style>
</head>
<body>
<div class="row">
<label for="cmd">Commands (one per line)</label>
<textarea id="cmd" spellcheck="false" placeholder="/repos-clone-sync https://…&#10;/workspace-sync my-repo"></textarea>
</div>
<div class="row">
<button type="button" id="run">Run</button>
<button type="button" id="clear">Clear output</button>
</div>
<label for="out">Response</label>
<pre id="out"></pre>
<script src="${scriptUri}"></script>
</body>
</html>`;
};
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<string, unknown>;
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,
);
};

View File

@ -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<void> => {
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 */
};

View File

@ -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<string[]> => {
const out: string[] = [];
const scan = async (d: string): Promise<void> => {
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<InitialRagImportResult> => {
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,
};
};

View File

@ -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<string, string> => {
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<unknown> => {
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<unknown> => {
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<string, unknown>;
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 };
};

View File

@ -1,7 +0,0 @@
export interface AnythingWorkspace {
readonly id: number;
readonly name: string;
readonly slug: string;
readonly createdAt?: string;
readonly lastUpdatedAt?: string;
}

View File

@ -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<EnsuredWorkspace> => {
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 };
};

View File

@ -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

View File

@ -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"]
}

View File

@ -56,6 +56,9 @@
"settings": { "settings": {
"smartIde.activeProjectId": "<name>" "smartIde.activeProjectId": "<name>"
} }
},
"preview_urls": {
"test": "https://<name>.test.example/"
} }
} }
} }

View File

@ -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 <id>]" >&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 <id>" >&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}"

View File

@ -1,6 +1,6 @@
# anythingllm-devtools # anythingllm-devtools
Service HTTP local : orchestration **repos-devtools-server** + API **AnythingLLM** (workspaces, upload documents initiaux selon `.4nkaiignore`). Remplace lusage principal de lextension 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 lancienne extension IDE AnythingLLM (voir [extension-anythingllm-workspaces.md](../../docs/repo/extension-anythingllm-workspaces.md)).
## Prérequis ## Prérequis

View File

@ -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 quun build Chromium complet nest 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 lhô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 <url>`** — ouvre lURL dans Carbonyl.
- Depuis la racine du monorepo : **`scripts/open-carbonyl-preview-test.sh`** — ouvre lURL **test** déclarée pour le projet actif (voir ci-dessous).
## Prévisualisation des déploiements **test**
Dans **`projects/<id>/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 <id>`**) 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`).

View File

@ -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") <url>}"
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}"

@ -0,0 +1 @@
Subproject commit ab80a276b1bd1c2c8dcefc8f248415dfc61dc2bf