Nicolas Cantu 69ab265560 feat: initial RAG sync with .4nkaiignore (extension 0.3, server 0.2)
**Motivations:**
- Seed AnythingLLM workspace from cloned repo using gitignore-style filters

**Root causes:**
- N/A

**Correctifs:**
- N/A

**Evolutions:**
- Template 4nkaiignore.default; server copies after clone; extension uploads via POST /api/v1/document/upload
- New commands /workspace-sync; settings initialSync*; dependency ignore

**Pages affectées:**
- extensions/anythingllm-workspaces/*
- services/repos-devtools-server/*
- docs/features/initial-rag-sync-4nkaiignore.md
2026-03-24 22:36:37 +01:00

123 lines
3.6 KiB
TypeScript

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