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:
parent
c4215044f0
commit
cb87e283a1
@ -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 |
|
||||
|
||||
28
docs/features/anythingllm-vscode-extension.md
Normal file
28
docs/features/anythingllm-vscode-extension.md
Normal file
@ -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`).
|
||||
3
extensions/anythingllm-workspaces/.gitignore
vendored
Normal file
3
extensions/anythingllm-workspaces/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
out/
|
||||
*.vsix
|
||||
5
extensions/anythingllm-workspaces/.vscodeignore
Normal file
5
extensions/anythingllm-workspaces/.vscodeignore
Normal file
@ -0,0 +1,5 @@
|
||||
.vscode/**
|
||||
src/**
|
||||
tsconfig.json
|
||||
.gitignore
|
||||
**/*.map
|
||||
38
extensions/anythingllm-workspaces/README.md
Normal file
38
extensions/anythingllm-workspaces/README.md
Normal 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 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`.
|
||||
55
extensions/anythingllm-workspaces/package.json
Normal file
55
extensions/anythingllm-workspaces/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
76
extensions/anythingllm-workspaces/src/anythingllmClient.ts
Normal file
76
extensions/anythingllm-workspaces/src/anythingllmClient.ts
Normal 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));
|
||||
};
|
||||
65
extensions/anythingllm-workspaces/src/extension.ts
Normal file
65
extensions/anythingllm-workspaces/src/extension.ts
Normal 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 */
|
||||
};
|
||||
14
extensions/anythingllm-workspaces/src/types.ts
Normal file
14
extensions/anythingllm-workspaces/src/types.ts
Normal 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[];
|
||||
}
|
||||
17
extensions/anythingllm-workspaces/tsconfig.json
Normal file
17
extensions/anythingllm-workspaces/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user