- 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
239 lines
7.2 KiB
TypeScript
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 };
|
|
};
|