Add AnythingLLM workspaces VS Code extension scaffold

**Motivations:**
- Expose AnythingLLM API workspaces from the editor against ia.enso public URL

**Root causes:**
- N/A (new capability)

**Correctifs:**
- N/A

**Evolutions:**
- Extension folder with list/open UI commands and API client
- Docs index and feature note

**Pages affectées:**
- extensions/anythingllm-workspaces/*
- docs/README.md
- docs/features/anythingllm-vscode-extension.md
This commit is contained in:
Nicolas Cantu 2026-03-23 11:10:15 +01:00
parent c4215044f0
commit cb87e283a1
10 changed files with 303 additions and 0 deletions

View File

@ -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 | | [infrastructure.md](./infrastructure.md) | Host inventory (LAN), SSH key workflow, host scripts |
| [services.md](./services.md) | Ollama, AnythingLLM (Docker), Desktop installer, Ollama ↔ Docker | | [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) | | [../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 lUI |
| [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 | | [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 | | [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 | | [ux-navigation-model.md](./ux-navigation-model.md) | Beyond file explorer: intentions, graph, palette, risks, expert mode |

View File

@ -0,0 +1,28 @@
# AnythingLLM workspaces — extension VS Code / Cursor
**Author:** 4NK
## Objectif
Fournir un point dentrée minimal dans léditeur pour lister les **workspaces AnythingLLM** via lAPI développeur (`GET /api/v1/workspaces`) et ouvrir linterface web du workspace sélectionné, en sappuyant sur lURL 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 lextension : 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 danalyse
- En cas déchec : lire le message derreur 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 lUI AnythingLLM.
- Référence API amont : Mintplex-Labs anything-llm, `server/endpoints/api/workspace/index.js` (`GET /v1/workspaces` sous préfixe `/api`).

View File

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

View File

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

View File

@ -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/<slug>` 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 Cursors 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`.

View File

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

View File

@ -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<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;
};
export const listWorkspaces = async (
baseUrl: string,
apiKey: string,
): Promise<readonly AnythingWorkspace[]> => {
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));
};

View File

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

View File

@ -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[];
}

View File

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