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