Nicolas Cantu b21ac2cf64 feat: anythingllm-devtools service, builazoo project, ssh-config example, docs
- Add services/anythingllm-devtools HTTP API (repos + AnythingLLM + RAG)
- Rename gitea-issues to git-issues across smart_ide agents and docs
- Add projects/builazoo, builazoo README, cron fragment, ssh-config.example
- Add ensure-ia-dev-project-link.sh; wrapper delegates smart_ide id
- Bump ia_dev submodule (git-issues rename, project symlinks)
- Align 4nkaiignore templates; update API index and project docs
2026-04-03 19:06:19 +02:00

239 lines
7.2 KiB
TypeScript

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<string> => {
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<string> => {
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<string, unknown>;
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<DevRunAggregate> => {
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 };
};