diff --git a/docs/README.md b/docs/README.md index 74f9ee5..2a08f5b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,6 +12,8 @@ Operational, architectural, and UX-design notes for the local-AI IDE initiative | [infrastructure.md](./infrastructure.md) | Host inventory (LAN), SSH key workflow, host scripts | | [services.md](./services.md) | Ollama, AnythingLLM (Docker), Desktop installer, Ollama ↔ Docker | | [../deploy/nginx/README-ia-enso.md](../deploy/nginx/README-ia-enso.md) | Proxy HTTPS `ia.enso.4nkweb.com` → Ollama / AnythingLLM (Bearer, script SSH, dépannage) | +| [../extensions/anythingllm-workspaces/README.md](../extensions/anythingllm-workspaces/README.md) | Extension VS Code / Cursor : lister les workspaces AnythingLLM (API) et ouvrir l’UI | +| [features/anythingllm-vscode-extension.md](./features/anythingllm-vscode-extension.md) | Fiche évolution : extension AnythingLLM, impacts, modalités | | [features/ia-enso-nginx-proxy-ollama-anythingllm.md](./features/ia-enso-nginx-proxy-ollama-anythingllm.md) | Fiche évolution : objectifs, impacts, modalités du reverse proxy ia.enso | | [anythingllm-workspaces.md](./anythingllm-workspaces.md) | One AnythingLLM workspace per project; sync pipeline | | [ux-navigation-model.md](./ux-navigation-model.md) | Beyond file explorer: intentions, graph, palette, risks, expert mode | diff --git a/docs/features/anythingllm-vscode-extension.md b/docs/features/anythingllm-vscode-extension.md new file mode 100644 index 0000000..cc7644c --- /dev/null +++ b/docs/features/anythingllm-vscode-extension.md @@ -0,0 +1,28 @@ +# AnythingLLM workspaces — extension VS Code / Cursor + +**Author:** 4NK + +## Objectif + +Fournir un point d’entrée minimal dans l’éditeur pour lister les **workspaces AnythingLLM** via l’API développeur (`GET /api/v1/workspaces`) et ouvrir l’interface web du workspace sélectionné, en s’appuyant sur l’URL publique documentée pour **ia.enso** (`/anythingllm/`). + +## Impacts + +- Nouveau répertoire : `extensions/anythingllm-workspaces/` (extension autonome, non publiée sur le marketplace par défaut). +- Aucun impact sur le déploiement nginx ni sur les services Docker tant que seuls les paramètres utilisateur (`baseUrl`, `apiKey`) sont renseignés côté poste développeur. + +## Modifications + +- `package.json`, `tsconfig.json`, sources TypeScript (`src/extension.ts`, `src/anythingllmClient.ts`, `src/types.ts`). +- `README.md` de l’extension : prérequis, configuration, commandes, lien vers `deploy/nginx/README-ia-enso.md`. + +## Modalités de déploiement + +- Développement : ouvrir le dossier `extensions/anythingllm-workspaces` dans VS Code / Cursor, `npm install`, `npm run compile`, lancer **Run Extension**. +- Distribution interne : `vsce package` après installation de `@vscode/vsce` si besoin, installation du `.vsix` sur les postes cibles. + +## Modalités d’analyse + +- En cas d’échec : lire le message d’erreur affiché par la commande (statut HTTP et extrait du corps). +- Vérifier côté proxy que `anythingllm.baseUrl` correspond au chemin public (sans slash final) et que la clé API est valide dans l’UI AnythingLLM. +- Référence API amont : Mintplex-Labs anything-llm, `server/endpoints/api/workspace/index.js` (`GET /v1/workspaces` sous préfixe `/api`). diff --git a/extensions/anythingllm-workspaces/.gitignore b/extensions/anythingllm-workspaces/.gitignore new file mode 100644 index 0000000..d3e15b1 --- /dev/null +++ b/extensions/anythingllm-workspaces/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +out/ +*.vsix diff --git a/extensions/anythingllm-workspaces/.vscodeignore b/extensions/anythingllm-workspaces/.vscodeignore new file mode 100644 index 0000000..ebb513a --- /dev/null +++ b/extensions/anythingllm-workspaces/.vscodeignore @@ -0,0 +1,5 @@ +.vscode/** +src/** +tsconfig.json +.gitignore +**/*.map diff --git a/extensions/anythingllm-workspaces/README.md b/extensions/anythingllm-workspaces/README.md new file mode 100644 index 0000000..6a2050f --- /dev/null +++ b/extensions/anythingllm-workspaces/README.md @@ -0,0 +1,38 @@ +# AnythingLLM Workspaces (VS Code / Cursor extension) + +Minimal extension to call the **AnythingLLM developer API** and open a workspace in the browser. + +## Prerequisites + +- AnythingLLM reachable at your public base URL (e.g. `https://ia.enso.4nkweb.com/anythingllm`). +- An **API key** created in AnythingLLM: **Settings → API Keys**. + +## Configuration + +| Setting | Description | +|--------|-------------| +| `anythingllm.baseUrl` | Base URL without trailing slash (default matches `deploy/nginx/README-ia-enso.md`). | +| `anythingllm.apiKey` | Bearer token for `GET /api/v1/workspaces`. Use **User** settings to avoid committing secrets. | + +## Commands + +- **AnythingLLM: List workspaces** — Fetches workspaces, then opens the selected one in the default browser (`/workspace/` under your base URL). +- **AnythingLLM: Open web UI** — Opens the AnythingLLM base URL. + +## Ollama + +This extension targets **AnythingLLM** only. For OpenAI-compatible Ollama behind the same proxy, use Cursor’s model settings with `https://ia.enso.4nkweb.com/ollama/v1` and the nginx Bearer (see `deploy/nginx/README-ia-enso.md`). + +## Build + +```bash +cd extensions/anythingllm-workspaces +npm install +npm run compile +``` + +Load the folder in VS Code / Cursor with **Run Extension** or install the packaged `.vsix` after `vsce package`. + +## API reference + +Upstream routes (mounted under `/api`): `GET /v1/workspaces` — see Mintplex-Labs anything-llm `server/endpoints/api/workspace/index.js`. diff --git a/extensions/anythingllm-workspaces/package.json b/extensions/anythingllm-workspaces/package.json new file mode 100644 index 0000000..048b10b --- /dev/null +++ b/extensions/anythingllm-workspaces/package.json @@ -0,0 +1,55 @@ +{ + "name": "anythingllm-workspaces", + "displayName": "AnythingLLM Workspaces (ia.enso)", + "description": "List AnythingLLM workspaces via your proxied instance (e.g. ia.enso.4nkweb.com/anythingllm).", + "version": "0.1.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 (nginx path `/anythingllm/`, no trailing slash required)." + }, + "anythingllm.apiKey": { + "type": "string", + "default": "", + "markdownDescription": "AnythingLLM API key (UI: **Settings → API Keys**). Prefer **User** settings to avoid committing secrets." + } + } + }, + "commands": [ + { + "command": "anythingllm.listWorkspaces", + "title": "AnythingLLM: List workspaces" + }, + { + "command": "anythingllm.openWebUi", + "title": "AnythingLLM: Open web UI" + } + ] + }, + "scripts": { + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "vscode:prepublish": "npm run compile" + }, + "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 new file mode 100644 index 0000000..fceb734 --- /dev/null +++ b/extensions/anythingllm-workspaces/src/anythingllmClient.ts @@ -0,0 +1,76 @@ +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; +}; + +export const listWorkspaces = async ( + baseUrl: string, + apiKey: string, +): Promise => { + const normalized = normalizeAnythingLlmBaseUrl(baseUrl); + const key = apiKey.trim(); + 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)); +}; diff --git a/extensions/anythingllm-workspaces/src/extension.ts b/extensions/anythingllm-workspaces/src/extension.ts new file mode 100644 index 0000000..5b94c9f --- /dev/null +++ b/extensions/anythingllm-workspaces/src/extension.ts @@ -0,0 +1,65 @@ +import * as vscode from "vscode"; +import { listWorkspaces, normalizeAnythingLlmBaseUrl } from "./anythingllmClient"; +import type { AnythingWorkspace } from "./types"; + +const CONFIG_SECTION = "anythingllm"; + +const readConfig = (): { 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 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 } = readConfig(); + 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 } = readConfig(); + 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}`); + } + }); + + context.subscriptions.push(listCmd, openUiCmd); +}; + +export const deactivate = (): void => { + /* no-op */ +}; diff --git a/extensions/anythingllm-workspaces/src/types.ts b/extensions/anythingllm-workspaces/src/types.ts new file mode 100644 index 0000000..7db47ba --- /dev/null +++ b/extensions/anythingllm-workspaces/src/types.ts @@ -0,0 +1,14 @@ +export interface AnythingThreadSummary { + readonly user_id: number | null; + readonly slug: string; + readonly name: string | null; +} + +export interface AnythingWorkspace { + readonly id: number; + readonly name: string; + readonly slug: string; + readonly createdAt?: string; + readonly lastUpdatedAt?: string; + readonly threads?: readonly AnythingThreadSummary[]; +} diff --git a/extensions/anythingllm-workspaces/tsconfig.json b/extensions/anythingllm-workspaces/tsconfig.json new file mode 100644 index 0000000..663c13e --- /dev/null +++ b/extensions/anythingllm-workspaces/tsconfig.json @@ -0,0 +1,17 @@ +{ + "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"] +}