import { devCommandsHelpText, parseDevCommandLine, type ParsedDevCommand, } from "./commandParser.js"; import { normalizeAnythingLlmBaseUrl } from "./anythingllmClient.js"; import { reposApiClone, reposApiList, reposApiLoad } from "./reposApiClient.js"; import { ensureWorkspaceForRepoName } from "./workspaceEnsure.js"; import { runInitialRagImportFromRepo } from "./initialRagSync.js"; import type { AnythingllmDevtoolsConfig } from "./env.js"; const DEFAULT_BRANCH = "test"; export type DevClientAction = | { readonly type: "openFolder"; readonly path: string } | { readonly type: "openWorkspaceUrl"; readonly url: string; readonly slug: string }; const fmt = (value: unknown): string => { if (typeof value === "string") { return value; } try { return JSON.stringify(value, null, 2); } catch { return String(value); } }; export interface DevRunnerContext extends AnythingllmDevtoolsConfig { readonly pushOpenFolder: (fsPath: string) => void; readonly pushOpenWorkspaceUrl: (slug: string) => void; } const assertReposConfig = (ctx: DevRunnerContext): void => { if (ctx.reposApiBaseUrl.trim().length === 0) { throw new Error("Set REPOS_DEVTOOLS_URL (repos-devtools-server URL)."); } if (ctx.reposApiToken.trim().length === 0) { throw new Error("Set REPOS_DEVTOOLS_TOKEN (same value as repos-devtools-server expects)."); } }; const assertAnythingConfig = (ctx: DevRunnerContext): void => { if (ctx.anythingApiKey.trim().length === 0) { throw new Error("Set ANYTHINGLLM_API_KEY for workspace operations."); } }; const appendInitialRag = async ( ctx: DevRunnerContext, repoRoot: string, workspaceSlug: string, ): Promise => { if (!ctx.initialSyncAfterClone) { return ""; } assertAnythingConfig(ctx); const res = await runInitialRagImportFromRepo({ baseUrl: ctx.anythingBaseUrl, apiKey: ctx.anythingApiKey, workspaceSlug, repoRoot, templateFsPath: ctx.default4nkaiignoreTemplateFsPath, maxFiles: ctx.initialSyncMaxFiles, maxFileBytes: ctx.initialSyncMaxFileBytes, }); return `\n---\nInitial RAG sync: ${fmt(res)}`; }; const workspaceUrlForSlug = (baseUrl: string, slug: string): string => { const root = normalizeAnythingLlmBaseUrl(baseUrl); return `${root}/workspace/${encodeURIComponent(slug)}`; }; const runOne = async (cmd: ParsedDevCommand, ctx: DevRunnerContext): Promise => { if (cmd.kind === "help") { return devCommandsHelpText(); } if (cmd.kind === "unknown") { return `Unknown command: ${cmd.raw}\n${devCommandsHelpText()}`; } if (cmd.kind === "repos-list") { assertReposConfig(ctx); const data = await reposApiList(ctx.reposApiBaseUrl, ctx.reposApiToken); return fmt(data); } if (cmd.kind === "repos-clone") { assertReposConfig(ctx); if (cmd.url.length === 0) { throw new Error("/repos-clone requires a git URL."); } const data = await reposApiClone( ctx.reposApiBaseUrl, ctx.reposApiToken, cmd.url, DEFAULT_BRANCH, ); let out = fmt(data); if (cmd.sync) { assertAnythingConfig(ctx); const rec = data as Record; const name = rec.name; const fsPath = rec.path; if (typeof name !== "string") { throw new Error("clone response missing name"); } if (typeof fsPath !== "string" || fsPath.length === 0) { throw new Error("clone response missing path"); } const ensured = await ensureWorkspaceForRepoName( ctx.anythingBaseUrl, ctx.anythingApiKey, name, ); out += `\n---\nAnythingLLM workspace: ${fmt({ slug: ensured.workspace.slug, name: ensured.workspace.name, workspaceCreatedByApi: ensured.created, })}`; out += await appendInitialRag(ctx, fsPath, ensured.workspace.slug); ctx.pushOpenFolder(fsPath); ctx.pushOpenWorkspaceUrl(ensured.workspace.slug); } return out; } if (cmd.kind === "repos-load") { assertReposConfig(ctx); if (cmd.name.length === 0) { throw new Error("/repos-load requires a repository folder name."); } const loaded = await reposApiLoad(ctx.reposApiBaseUrl, ctx.reposApiToken, cmd.name); let out = fmt(loaded); ctx.pushOpenFolder(loaded.path); if (cmd.sync) { assertAnythingConfig(ctx); const ensured = await ensureWorkspaceForRepoName( ctx.anythingBaseUrl, ctx.anythingApiKey, loaded.name, ); out += `\n---\nAnythingLLM workspace: ${fmt({ slug: ensured.workspace.slug, name: ensured.workspace.name, workspaceCreatedByApi: ensured.created, })}`; out += await appendInitialRag(ctx, loaded.path, ensured.workspace.slug); ctx.pushOpenWorkspaceUrl(ensured.workspace.slug); } return out; } if (cmd.kind === "workspace-load") { assertAnythingConfig(ctx); if (cmd.name.length === 0) { throw new Error("/workspace-load requires a workspace name."); } const ensured = await ensureWorkspaceForRepoName( ctx.anythingBaseUrl, ctx.anythingApiKey, cmd.name, ); ctx.pushOpenWorkspaceUrl(ensured.workspace.slug); return fmt({ slug: ensured.workspace.slug, name: ensured.workspace.name, workspaceCreatedByApi: ensured.created, }); } if (cmd.kind === "workspace-sync-repo") { assertReposConfig(ctx); assertAnythingConfig(ctx); if (cmd.name.length === 0) { throw new Error("/workspace-sync requires a repository folder name."); } const loaded = await reposApiLoad(ctx.reposApiBaseUrl, ctx.reposApiToken, cmd.name); const ensured = await ensureWorkspaceForRepoName( ctx.anythingBaseUrl, ctx.anythingApiKey, loaded.name, ); let out = fmt({ repoPath: loaded.path, slug: ensured.workspace.slug, name: ensured.workspace.name, workspaceCreatedByApi: ensured.created, }); out += await appendInitialRag(ctx, loaded.path, ensured.workspace.slug); return out; } return `Unhandled: ${JSON.stringify(cmd)}`; }; export interface DevRunAggregate { readonly text: string; readonly actions: readonly DevClientAction[]; } export const runDevToolsScript = async ( text: string, base: AnythingllmDevtoolsConfig, ): Promise => { const actions: DevClientAction[] = []; const ctx: DevRunnerContext = { ...base, pushOpenFolder: (p: string) => { if (p.length > 0) { actions.push({ type: "openFolder", path: p }); } }, pushOpenWorkspaceUrl: (slug: string) => { if (slug.length > 0) { actions.push({ type: "openWorkspaceUrl", slug, url: workspaceUrlForSlug(base.anythingBaseUrl, slug), }); } }, }; const lines = text .split("\n") .map((l) => l.trim()) .filter((l) => l.length > 0); if (lines.length === 0) { return { text: devCommandsHelpText(), actions: [] }; } const parts: string[] = []; for (const line of lines) { const parsed = parseDevCommandLine(line); try { parts.push(await runOne(parsed, ctx)); } catch (e) { const msg = e instanceof Error ? e.message : String(e); parts.push(`ERROR: ${msg}`); } } return { text: parts.join("\n---\n"), actions }; };