UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

585 lines (584 loc) • 26.2 kB
import { l as normalizeOptionalString, m as readNonBlankString } from "./string-coerce-6kR-86XC.mjs"; import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { t as pruneMapToMaxSize } from "./map-size-CNcWiFKu.mjs"; import { r as isPathInside } from "./path-guards-CH7noY0Z.mjs"; import { t as formatErrorMessage } from "./errors-BSdPMFao.mjs"; import { t as areDiagnosticsEnabledForProcess } from "./diagnostic-events-BflWr4hm.mjs"; import { A as parseAgentSessionKey } from "./session-key-BC4m_Ly5.mjs"; import { u as WRITE_SCOPE } from "./operator-scopes-D-CL26h0.mjs"; import { t as runTasksWithConcurrency } from "./run-with-concurrency-Dtu208ef.mjs"; import { d as isTrustedSecretSurfaceUnavailableError } from "./runtime-degraded-state-oevwKU3u.mjs"; import { Cr as validateProjectsRemoveParams, Sr as validateProjectsRegisterParams, br as validateProjectsAddParams, wr as validateProjectsSearchRemoteParams, xr as validateProjectsListParams } from "./src-D2gvuSyP.mjs"; import { v as readCurrentUserProfileAliases } from "./user-profile-list-y8HW3dT5.mjs"; import { r as GatewayErrorDetailCodes, t as ErrorCodes } from "./gateway-error-details-DIefe7Zy.mjs"; import { f as errorShape } from "./error-codes-DI20vDEB.mjs"; import { v as readGatewayAccessRevision } from "./operator-role-policy-C96xz3qI.mjs"; import { s as sessionCreatorProfileId } from "./session-entry-provenance-BWZtrWXT.mjs"; import { I as getSessionRepositoryWorkspaceStore } from "./session-accessor.sqlite-entry-store-BVV5x7kL.mjs"; import { r as authorizeOperatorScopesForRequiredScope } from "./method-scopes-C-MKLkWf.mjs"; import { t as createStageTimingTracker } from "./stage-timing-BUG4Fnsv.mjs"; import { n as getSessionRowProjection } from "./session-row-projection-access-CUCrJD4M.mjs"; import { a as managedWorktrees } from "./service-q3EuGfAd.mjs"; import { g as listRegistryWorktrees } from "./registry-CvH4JdSJ.mjs"; import { n as loadCombinedSessionStoreForGatewayCore, r as loadCombinedSessionStoreForGatewayCoreAsync } from "./combined-store-gateway-DrsLuuM9.mjs"; import { b as createSessionListEntryFilter } from "./session-sharing-KNQSWoOJ.mjs"; import "./session-utils-DrY1lc9W.mjs"; import { a as removeProjectRegistry, n as listWorkspaceProjects, p as ProjectCheckoutError, r as registerProjectRegistry, s as resolveProjectRegistry, t as listProjectRegistry } from "./project-registry-3aLiY1zE.mjs"; import { n as gitHubPublicApi, r as githubApiToken, t as CONTROL_UI_GITHUB_CREDENTIAL_UNAVAILABLE_MESSAGE } from "./github-public-api-D6N-IT-v.mjs"; import { t as assertValidParams } from "./validation-B7TobU6-.mjs"; import { i as ProjectCloneError, r as removeClonedProjectCheckout, t as materializeProjectClone } from "./project-clone-Cm5iXEna.mjs"; import { t as parseProjectGitUrl } from "./project-git-url-D2byNTeD.mjs"; import path from "node:path"; import { channel } from "node:diagnostics_channel"; import { performance } from "node:perf_hooks"; //#region src/gateway/project-github-search.ts const SEARCH_CACHE_MS = 6e4; const SEARCH_CACHE_LIMIT = 100; const SEARCH_RESULT_LIMIT = 10; const AFFILIATED_RESULT_LIMIT = 10; const EXACT_REPO_QUERY = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\/[A-Za-z0-9._-]+$/; const searchCache = /* @__PURE__ */ new Map(); function boundedString(value, maxLength) { const trimmed = value?.trim(); return trimmed ? trimmed.slice(0, maxLength) : void 0; } function parseRepository(value) { if (!isRecord(value)) return null; const fullName = readNonBlankString(value.full_name); const name = readNonBlankString(value.name); if (!fullName || !name) return null; const clone = parseProjectGitUrl(readNonBlankString(value.clone_url) ?? ""); const webUrl = boundedString(readNonBlankString(value.html_url), 2048); if (!clone || !webUrl) return null; const description = boundedString(readNonBlankString(value.description), 500); return { name: name.slice(0, 100), fullName: fullName.slice(0, 200), cloneUrl: clone.url, webUrl, private: value.private === true, ...description ? { description } : {} }; } function repositoryArray(value) { return (Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.items) ? value.items : []).flatMap((item) => { const parsed = parseRepository(item); return parsed ? [parsed] : []; }); } function matchesAffiliatedQuery(project, query) { const needle = query.toLowerCase(); return [ project.name, project.fullName, project.description ?? "" ].join("\n").toLowerCase().includes(needle); } async function loadExactRepository(query, fetchImpl, token) { const url = new URL(`/repos/${query}`, gitHubPublicApi.GITHUB_API_ORIGIN); try { return parseRepository(await gitHubPublicApi.fetchGitHubJson(url.href, fetchImpl, token)); } catch { return null; } } async function loadAffiliatedRepositories(fetchImpl, token) { const url = new URL("/user/repos", gitHubPublicApi.GITHUB_API_ORIGIN); url.searchParams.set("affiliation", "owner,collaborator,organization_member"); url.searchParams.set("sort", "updated"); url.searchParams.set("direction", "desc"); url.searchParams.set("per_page", String(AFFILIATED_RESULT_LIMIT)); try { const response = await gitHubPublicApi.fetchGitHubApi(url.href, fetchImpl, token); return repositoryArray(await gitHubPublicApi.readGitHubJsonResponse(response)); } catch { return []; } } async function loadRepositorySearch(query, fetchImpl, token) { const url = new URL("/search/repositories", gitHubPublicApi.GITHUB_API_ORIGIN); url.searchParams.set("q", `${query} in:name,description`); url.searchParams.set("per_page", String(SEARCH_RESULT_LIMIT)); return repositoryArray(await gitHubPublicApi.fetchGitHubJson(url.href, fetchImpl, token)); } async function searchProjectsUncached(params) { const [exact, affiliated, global] = await Promise.all([ EXACT_REPO_QUERY.test(params.query) ? loadExactRepository(params.query, params.fetchImpl, params.token) : null, params.token ? loadAffiliatedRepositories(params.fetchImpl, params.token) : [], loadRepositorySearch(params.query, params.fetchImpl, params.token) ]); const ranked = [ ...exact ? [exact] : [], ...affiliated.filter((project) => matchesAffiliatedQuery(project, params.query)), ...global ]; const deduped = /* @__PURE__ */ new Map(); for (const project of ranked) { const key = project.fullName.toLowerCase(); if (!deduped.has(key)) deduped.set(key, project); } return { credential: params.token ? "configured" : "missing", projects: [...deduped.values()].slice(0, SEARCH_RESULT_LIMIT) }; } /** Searches affiliated and public GitHub repositories for the project picker. */ function searchRemoteProjects(query, options = {}) { const normalizedQuery = query.trim().toLowerCase(); const { token, cacheScope } = gitHubPublicApi.resolveGitHubApiCredentialScope(options.env); const cacheKey = `${normalizedQuery}\0${cacheScope}`; const now = options.now ?? Date.now(); const cached = searchCache.get(cacheKey); if (cached && cached.expiresAt > now) { searchCache.delete(cacheKey); searchCache.set(cacheKey, cached); return cached.promise; } const promise = searchProjectsUncached({ query: query.trim(), fetchImpl: options.fetchImpl ?? fetch, token }).catch((error) => { if (searchCache.get(cacheKey)?.promise === promise) searchCache.delete(cacheKey); throw error; }); searchCache.set(cacheKey, { expiresAt: now + SEARCH_CACHE_MS, promise }); pruneMapToMaxSize(searchCache, SEARCH_CACHE_LIMIT); return promise; } //#endregion //#region src/gateway/server-methods/projects-list-diagnostics.ts const diagnostics = channel("openclaw.projects.list"); function startProjectsListDiagnostics(context) { if (!areDiagnosticsEnabledForProcess() && !diagnostics.hasSubscribers) return; const timing = createStageTimingTracker(() => performance.now()); let phase = "registry"; const mark = (next) => { timing.mark(phase); phase = next; }; return { mark, finish() { timing.mark(phase); const { totalMs, stages } = timing.snapshot(); const fields = { operation: "projects.list", elapsedMs: totalMs, phaseDurationsMs: Object.fromEntries(stages.map(({ name, durationMs }) => [name, durationMs])) }; try { if (diagnostics.hasSubscribers) diagnostics.publish(fields); if (totalMs >= 1e3 && areDiagnosticsEnabledForProcess()) context.logGateway?.warn("projects.list: slow request", fields); } catch {} } }; } //#endregion //#region src/gateway/server-methods/projects-recents.ts function folderDisplayName(folder) { return folder.replace(/[\\/]+$/u, "").split(/[\\/]/u).at(-1) || folder; } function indexPathProjects(projects) { const byPath = /* @__PURE__ */ new Map(); const byAgent = /* @__PURE__ */ new Map(); for (const project of projects) { if (project.source === "workspace") byAgent.set(project.agentId, project); const previous = byPath.get(project.repoRoot); if (!previous || (Number(project.source === "workspace") - Number(previous.source === "workspace") || project.id.localeCompare(previous.id)) < 0) byPath.set(project.repoRoot, project); } return { byPath, byAgent }; } function listProjectRecents(store, profileIds, projects) { const candidates = Object.entries(store).filter(([, entry]) => Boolean(sessionCreatorProfileId(entry.createdActor)) && Boolean(entry.createdActor?.id && profileIds.has(entry.createdActor.id))).toSorted(([leftKey, left], [rightKey, right]) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0) || leftKey.localeCompare(rightKey)); const projectsById = new Map(projects.map((project) => [project.id, project])); const seen = /* @__PURE__ */ new Set(); const recents = []; let pathProjects; for (const [sessionKey, entry] of candidates) { if (entry.repositoryWorkspaceId) { const repository = getSessionRepositoryWorkspaceStore().get(entry.repositoryWorkspaceId); const sessionAgentId = parseAgentSessionKey(sessionKey)?.agentId; if (!repository || repository.sessionKey !== sessionKey || sessionAgentId && repository.agentId !== sessionAgentId || seen.has(repository.url)) continue; seen.add(repository.url); recents.push({ kind: "repository", url: repository.url, displayName: path.posix.basename(repository.url, ".git") }); if (recents.length === 8) break; continue; } const projectId = normalizeOptionalString(entry.projectId); const explicitProject = projectId ? projectsById.get(projectId) : void 0; const worktreeRoot = normalizeOptionalString(entry.worktree?.repoRoot); const spawnedCwd = normalizeOptionalString(entry.spawnedCwd); const execCwd = normalizeOptionalString(entry.execCwd); const folder = worktreeRoot ?? spawnedCwd ?? execCwd; let project = explicitProject; if (!project && folder) { const agentId = parseAgentSessionKey(sessionKey)?.agentId; const indexed = pathProjects ??= indexPathProjects(projects); const workspace = indexed.byAgent.get(agentId); project = workspace?.repoRoot === folder ? workspace : indexed.byPath.get(folder); } const key = project ? `project:${project.id}` : folder ? `folder:${normalizeOptionalString(entry.execNode) ?? ""}\0${folder}` : void 0; if (!key || seen.has(key)) continue; seen.add(key); recents.push(project ? { kind: "project", projectId: project.id, displayName: project.displayName } : { kind: "folder", folder, displayName: folderDisplayName(folder), ...normalizeOptionalString(entry.execNode) ? { execNode: normalizeOptionalString(entry.execNode) } : {} }); if (recents.length === 8) break; } return recents; } //#endregion //#region src/gateway/server-methods/projects.ts const PROJECTS_LIST_MAX_RAW_CANDIDATES = Math.max(50, 50, 32); function checkoutName(checkoutPath) { const trimmed = checkoutPath.replace(/[\\/]+$/u, ""); return trimmed.split(/[\\/]/u).at(-1) || trimmed; } function compareRawProjectCandidates(left, right) { return right.lastUsedAt - left.lastUsedAt || left.checkoutPath.localeCompare(right.checkoutPath) || left.kind.localeCompare(right.kind); } function retainNewestRawProjectCandidate(candidates, candidate) { const insertionIndex = candidates.findIndex((existing) => compareRawProjectCandidates(candidate, existing) < 0); if (insertionIndex < 0) { if (candidates.length < PROJECTS_LIST_MAX_RAW_CANDIDATES) candidates.push(candidate); return; } candidates.splice(insertionIndex, 0, candidate); if (candidates.length > PROJECTS_LIST_MAX_RAW_CANDIDATES) candidates.pop(); } function sanitizePublicOriginUrl(originUrl) { const trimmed = originUrl.trim(); const suffixIndex = trimmed.search(/[?#]/u); const withoutSuffix = suffixIndex < 0 ? trimmed : trimmed.slice(0, suffixIndex); const scp = /^[^@\s/:]+@(\[[^\]]+\]|[^:\s]+):(.+)$/u.exec(withoutSuffix); if (scp) return `${scp[1]}:${scp[2]}`; let parsed; try { parsed = new URL(withoutSuffix); } catch { return; } if (!parsed.username && !parsed.password) return withoutSuffix; parsed.username = ""; parsed.password = ""; return parsed.toString(); } function sanitizeProjectRecord(project) { const { originUrl, ...record } = project; const sanitizedOriginUrl = originUrl ? sanitizePublicOriginUrl(originUrl) : void 0; return { ...record, ...sanitizedOriginUrl ? { originUrl: sanitizedOriginUrl } : {} }; } function projectCandidatesToSummaries(candidates) { const groups = /* @__PURE__ */ new Map(); for (const candidate of candidates) { const group = groups.get(candidate.fingerprint) ?? { checkouts: /* @__PURE__ */ new Map(), lastUsedAt: candidate.lastUsedAt, name: checkoutName(candidate.checkoutPath), nameUsedAt: candidate.lastUsedAt }; const checkout = group.checkouts.get(candidate.checkoutPath); if (!checkout || candidate.lastUsedAt > checkout.lastUsedAt) group.checkouts.set(candidate.checkoutPath, { path: candidate.checkoutPath, lastUsedAt: candidate.lastUsedAt }); group.lastUsedAt = Math.max(group.lastUsedAt, candidate.lastUsedAt); if (candidate.lastUsedAt > group.nameUsedAt) { group.name = checkoutName(candidate.checkoutPath); group.nameUsedAt = candidate.lastUsedAt; } if (!group.originUrl && candidate.originUrl) group.originUrl = candidate.originUrl; groups.set(candidate.fingerprint, group); } return [...groups.values()].toSorted((left, right) => right.lastUsedAt - left.lastUsedAt || left.name.localeCompare(right.name)).slice(0, 50).map((group) => { const summary = { name: group.name, checkouts: [...group.checkouts.values()].toSorted((left, right) => right.lastUsedAt - left.lastUsedAt || left.path.localeCompare(right.path)).slice(0, 50).map((checkout) => ({ runnerId: "gateway", path: checkout.path })), lastUsedAt: group.lastUsedAt }; if (group.originUrl) { const originUrl = sanitizePublicOriginUrl(group.originUrl); if (originUrl) summary.originUrl = originUrl; } return summary; }); } async function listObservedProjects(service, context, client, store, diagnostics) { diagnostics?.mark("worktreeRegistry"); const worktrees = await service.listRegistryRecords(); diagnostics?.mark("sessionCandidates"); const cfg = context.getRuntimeConfig(); const rawCandidates = []; const visibilityFilter = createSessionListEntryFilter({ client, cfg }); const canSeeAll = !visibilityFilter; for (const [sessionKey, entry] of Object.entries(store)) { if (visibilityFilter && !visibilityFilter(sessionKey, entry)) continue; const checkoutPath = entry.execCwd?.trim(); if (checkoutPath && !entry.execNode?.trim()) retainNewestRawProjectCandidate(rawCandidates, { kind: "session", checkoutPath, lastUsedAt: entry.updatedAt }); } diagnostics?.mark("worktreeCandidates"); for (const worktree of worktrees) { if (worktree.removedAt !== void 0) continue; if (!canSeeAll) { const ownerId = worktree.ownerKind === "session" ? worktree.ownerId?.trim() : void 0; const ownerEntry = ownerId ? store[ownerId] : void 0; if (!ownerId || !ownerEntry || !visibilityFilter?.(ownerId, ownerEntry)) continue; } retainNewestRawProjectCandidate(rawCandidates, { kind: "worktree", checkoutPath: worktree.path, fingerprint: worktree.repoFingerprint, lastUsedAt: worktree.lastActiveAt, repoRoot: worktree.repoRoot }); } diagnostics?.mark("identityProbes"); const probePaths = [...new Set(rawCandidates.map((raw) => raw.kind === "worktree" ? raw.repoRoot : raw.checkoutPath))].slice(0, 32); const { results } = await runTasksWithConcurrency({ tasks: probePaths.map((checkoutPath) => () => service.resolveRepositoryIdentity(checkoutPath)), limit: 4 }); const identities = new Map(probePaths.map((checkoutPath, index) => [checkoutPath, results[index]])); diagnostics?.mark("candidateProcessing"); const candidates = []; for (const raw of rawCandidates) { const identity = identities.get(raw.kind === "worktree" ? raw.repoRoot : raw.checkoutPath); if (raw.kind === "worktree") { candidates.push({ checkoutPath: raw.checkoutPath, fingerprint: raw.fingerprint, lastUsedAt: raw.lastUsedAt, ...identity?.originUrl ? { originUrl: identity.originUrl } : {} }); continue; } if (identity) candidates.push({ checkoutPath: identity.checkoutRoot, fingerprint: identity.fingerprint, lastUsedAt: raw.lastUsedAt, ...identity.originUrl ? { originUrl: identity.originUrl } : {} }); } return projectCandidatesToSummaries(candidates); } function findProjectCheckoutReference(cfg, repoRoot) { const normalizedRoot = path.resolve(repoRoot); const workspaceReference = listWorkspaceProjects(cfg).find((candidate) => path.resolve(candidate.repoRoot) === normalizedRoot); const worktreeReference = listRegistryWorktrees(process.env).find((worktree) => !worktree.removedAt && path.resolve(worktree.repoRoot) === normalizedRoot); const sessionReference = Object.entries(loadCombinedSessionStoreForGatewayCore(cfg, { projection: "list" }).store).find(([, entry]) => { if (entry.archivedAt) return false; const sessionRoot = entry.worktree?.repoRoot; if (sessionRoot && path.resolve(sessionRoot) === normalizedRoot) return true; const cwd = entry.spawnedCwd; return Boolean(cwd && (path.resolve(cwd) === normalizedRoot || isPathInside(normalizedRoot, path.resolve(cwd)))); }); return workspaceReference ? `agent workspace ${workspaceReference.displayName}` : worktreeReference ? `managed worktree ${worktreeReference.name}` : sessionReference ? `session ${sessionReference[0]}` : void 0; } function createProjectsHandlers(service) { return { "projects.list": async ({ params, respond, context, client }) => { if (!assertValidParams(params, validateProjectsListParams, "projects.list", respond)) return; const diagnostics = startProjectsListDiagnostics(context); try { const registryProjects = await listProjectRegistry(context.getRuntimeConfig()); diagnostics?.mark("sessions"); const projects = registryProjects.map(sanitizeProjectRecord); const cfg = context.getRuntimeConfig(); const requesterProfileId = client?.authenticatedUserProfile?.profileId; const requesterUserId = client?.authenticatedUserId; const accessRevision = readGatewayAccessRevision(); const assertCurrent = () => { if (client?.authenticatedUserProfile?.profileId !== requesterProfileId || client?.authenticatedUserId !== requesterUserId || readGatewayAccessRevision() !== accessRevision || context.getRuntimeConfig() !== cfg) throw new Error("Project access changed while preparing the listing. Retry the request."); }; const canWrite = () => authorizeOperatorScopesForRequiredScope(WRITE_SCOPE, Array.isArray(client?.connect.scopes) ? client.connect.scopes : []).allowed; let store = {}; let observedProjects; try { if (client?.authenticatedUserProfile?.profileId || params.includeObserved && canWrite()) { if (params.includeObserved) store = (await loadCombinedSessionStoreForGatewayCoreAsync(cfg, { projection: "list" })).store; else { const projection = getSessionRowProjection(context); if (!projection) throw new Error("Session projection is unavailable before Gateway startup completes"); do await projection.ensureMaterialized(); while (projection.needsMaterialization); assertCurrent(); if (getSessionRowProjection(context) !== projection || projection.state.cfg !== cfg) throw new Error("Session projection changed while preparing the listing. Retry the request."); store = loadCombinedSessionStoreForGatewayCore(cfg, { projection: "list", loadEntries: (target) => projection.selectEntries({ storePath: target.storePath, sortBy: null }).map((row) => ({ sessionKey: row.key, entry: row.storedEntry ?? row.entry, keyBytes: Buffer.from(row.key) })).toSorted((left, right) => Buffer.compare(left.keyBytes, right.keyBytes)) }).store; } assertCurrent(); } if (params.includeObserved && canWrite()) { observedProjects = await listObservedProjects(service, context, client, store, diagnostics); assertCurrent(); } } catch (error) { respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, formatErrorMessage(error))); return; } diagnostics?.mark("recents"); const profileId = client?.authenticatedUserProfile?.profileId; const recentProfileIds = profileId ? readCurrentUserProfileAliases(profileId) : void 0; const recents = recentProfileIds ? listProjectRecents(store, recentProfileIds, registryProjects) : void 0; diagnostics?.mark("response"); if (canWrite()) { respond(true, { projects, ...recents ? { recents } : {}, ...observedProjects ? { observedProjects } : {} }, void 0); return; } respond(true, { projects: projects.map(({ id, displayName, source, agentId }) => agentId ? { id, displayName, source, agentId } : { id, displayName, source }), ...recents ? { recents: recents.filter((recent) => recent.kind === "project") } : {} }, void 0); } finally { diagnostics?.finish(); } }, "projects.register": async ({ params, respond }) => { if (!assertValidParams(params, validateProjectsRegisterParams, "projects.register", respond)) return; try { respond(true, sanitizeProjectRecord(await registerProjectRegistry({ path: params.path, name: params.name })), void 0); } catch (error) { respond(false, void 0, errorShape(error instanceof ProjectCheckoutError ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, formatErrorMessage(error))); } }, "projects.add": async ({ params, respond, context, signal }) => { if (!assertValidParams(params, validateProjectsAddParams, "projects.add", respond)) return; try { respond(true, await materializeProjectClone({ cfg: context.getRuntimeConfig(), gitUrl: params.gitUrl, name: params.name }, { signal, token: githubApiToken() }), void 0); } catch (error) { if (isTrustedSecretSurfaceUnavailableError(error)) { respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, CONTROL_UI_GITHUB_CREDENTIAL_UNAVAILABLE_MESSAGE, { details: { code: GatewayErrorDetailCodes.PROJECT_CLONE_FAILED, cause: "auth_required" }, retryable: false })); return; } if (error instanceof ProjectCloneError) { respond(false, void 0, errorShape(error.failure === "invalid_url" ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, error.message, { details: { code: GatewayErrorDetailCodes.PROJECT_CLONE_FAILED, cause: error.failure }, retryable: error.failure === "network" || error.failure === "clone_failed" })); return; } respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, formatErrorMessage(error))); } }, "projects.searchRemote": async ({ params, respond }) => { if (!assertValidParams(params, validateProjectsSearchRemoteParams, "projects.searchRemote", respond)) return; try { respond(true, await searchRemoteProjects(params.query), void 0); } catch (error) { const { message, ...details } = error instanceof gitHubPublicApi.ControlUiGitHubError || isTrustedSecretSurfaceUnavailableError(error) ? gitHubPublicApi.formatControlUiGitHubPreviewError(error) : { message: "GitHub project search is unavailable. Retry shortly.", retryable: true }; respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, message, details)); } }, "projects.remove": async ({ params, respond, context }) => { if (!assertValidParams(params, validateProjectsRemoveParams, "projects.remove", respond)) return; const respondUnknownProject = () => { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `unknown project id: ${params.id}`)); }; const project = await resolveProjectRegistry(context.getRuntimeConfig(), params.id); if (!project || project.source === "workspace") { respondUnknownProject(); return; } let removed; if (params.deleteCheckout) { if (project.source !== "cloned") { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "Only projects cloned by the Gateway can delete their checkout.")); return; } try { removed = await removeClonedProjectCheckout(project, () => { const reference = findProjectCheckoutReference(context.getRuntimeConfig(), project.repoRoot); if (reference) throw new ProjectCheckoutError(`Project checkout is still referenced by ${reference}. Remove that reference before deleting the checkout.`); }); } catch (error) { respond(false, void 0, errorShape(error instanceof ProjectCheckoutError ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, formatErrorMessage(error))); return; } } else removed = await removeProjectRegistry(project); if (!removed) { respondUnknownProject(); return; } respond(true, { removed: true }, void 0); } }; } const projectsHandlers = createProjectsHandlers(managedWorktrees); //#endregion export { projectsHandlers };