openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
634 lines (633 loc) • 29.2 kB
JavaScript
import { l as normalizeOptionalString, m as readNonBlankString } from "./string-coerce-1VuiLaaR.mjs";
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import { t as pruneMapToMaxSize } from "./map-size-BlrH_CnE.mjs";
import { r as isPathInside } from "./path-guards-CH7noY0Z.mjs";
import { o as racePromiseWithAbortSignal } from "./src-D1BHGNTj.mjs";
import "./abort-signal-QqAmLWTl.mjs";
import { t as formatErrorMessage } from "./errors-O33D01XW.mjs";
import { t as areDiagnosticsEnabledForProcess } from "./diagnostic-events-BADdmgZf.mjs";
import { M as parseAgentSessionKey } from "./session-key-C09ow4h0.mjs";
import { o as getRuntimeConfigSnapshot } from "./runtime-snapshot-DYptB0Cf.mjs";
import { u as WRITE_SCOPE } from "./operator-scopes-DsrPoRc4.mjs";
import { d as isTrustedSecretSurfaceUnavailableError } from "./runtime-degraded-state-oevwKU3u.mjs";
import { Cr as validateProjectsRegisterParams, Sr as validateProjectsListParams, Tr as validateProjectsSearchRemoteParams, wr as validateProjectsRemoveParams, xr as validateProjectsAddParams } from "./src-l2dWvEsN.mjs";
import { v as readCurrentUserProfileAliases } from "./user-profile-list-CtETKur7.mjs";
import { r as GatewayErrorDetailCodes, t as ErrorCodes } from "./gateway-error-details-DIefe7Zy.mjs";
import { f as errorShape } from "./error-codes-Dh56LniQ.mjs";
import { g as readGatewayAccessRevision } from "./operator-role-policy-Bsa4SHqC.mjs";
import { s as sessionCreatorProfileId } from "./session-entry-provenance-Bfs2J1E7.mjs";
import { k as getSessionRepositoryWorkspaceStore } from "./session-accessor.sqlite-entry-store-CS34_uoa.mjs";
import { n as authorizeOperatorScopesForMethod, r as authorizeOperatorScopesForRequiredScope } from "./method-scopes-BrMPjfvW.mjs";
import { t as createStageTimingTracker } from "./stage-timing-BUG4Fnsv.mjs";
import { i as resolveConfiguredGitHubHost, r as resolveConfiguredGitHubApiBaseUrl } from "./github-host-CaivEi-a.mjs";
import { o as readCachedNativeGitHubToken } from "./github-read-identity-BTVIf5kE.mjs";
import { n as loadCombinedSessionStoreForGatewayCore, r as loadCombinedSessionStoreForGatewayCoreAsync } from "./combined-store-gateway-FSk6DpW_.mjs";
import { n as getSessionRowProjection, r as requireSessionRowProjection } from "./session-row-projection-access-C2RDnj28.mjs";
import { l as createSessionListEntryFilter } from "./session-sharing-target-read-B1Z4GF6o.mjs";
import "./session-sharing-C16h_pTr.mjs";
import { a as managedWorktrees } from "./service-CaFJ6NH4.mjs";
import { g as listRegistryWorktrees } from "./registry-Bm5FVBwN.mjs";
import { f as ProjectCheckoutError, i as removeProjectRegistry, n as listWorkspaceProjects, o as resolveProjectRegistry, r as registerProjectRegistry, t as listProjectRegistry } from "./project-registry-Ct_thE4K.mjs";
import "./session-utils-seGz8XhX.mjs";
import { n as gitHubPublicApi, r as githubApiToken, t as CONTROL_UI_GITHUB_CREDENTIAL_UNAVAILABLE_MESSAGE } from "./github-public-api-COOMiXSo.mjs";
import { i as ProjectCloneError, r as removeClonedProjectCheckout, t as materializeProjectClone } from "./project-clone-B9t9sWtU.mjs";
import { t as parseConfiguredProjectGitUrl } from "./project-git-url.runtime.js";
import { n as defineValidatedGatewayHandler, t as assertValidParams } from "./validation-Bu27N0nv.mjs";
import { t as createGitHubReadGroup } from "./control-ui-session-pr-request-BTQOuS8L.mjs";
import { t as configuredDefaultRepository } from "./configured-default-repository-BRG3U3Te.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) {
return normalizeOptionalString(value)?.slice(0, maxLength);
}
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 = parseConfiguredProjectGitUrl(readNonBlankString(value.clone_url) ?? "");
const webUrl = boundedString(value.html_url, 2048);
if (!clone || !webUrl) return null;
const description = boundedString(value.description, 500);
const defaultBranch = boundedString(value.default_branch, 255);
return {
name: name.slice(0, 100),
fullName: fullName.slice(0, 200),
cloneUrl: clone.url,
webUrl,
private: value.private === true,
...description ? { description } : {},
...defaultBranch ? { defaultBranch } : {}
};
}
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, request) {
const url = new URL(`repos/${query}`, `${gitHubPublicApi.GITHUB_API_BASE_URL}/`);
try {
return parseRepository(await request(url.href));
} catch {
return null;
}
}
async function loadAffiliatedRepositories(request) {
const url = new URL("user/repos", `${gitHubPublicApi.GITHUB_API_BASE_URL}/`);
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 {
return repositoryArray(await request(url.href, false));
} catch {
return [];
}
}
async function loadRepositorySearch(query, request) {
const url = new URL("search/repositories", `${gitHubPublicApi.GITHUB_API_BASE_URL}/`);
url.searchParams.set("q", `${query} in:name,description`);
url.searchParams.set("per_page", String(SEARCH_RESULT_LIMIT));
return repositoryArray(await request(url.href));
}
async function searchProjectsUncached(params) {
const [exact, affiliated, global] = await Promise.all([
EXACT_REPO_QUERY.test(params.query) ? loadExactRepository(params.query, params.request) : null,
params.token ? loadAffiliatedRepositories(params.request) : [],
loadRepositorySearch(params.query, params.request)
]);
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. */
async function searchRemoteProjects(query, options = {}) {
const config = getRuntimeConfigSnapshot();
const host = options.host ?? resolveConfiguredGitHubHost(config);
const apiBaseUrl = options.apiBaseUrl ?? resolveConfiguredGitHubApiBaseUrl(config);
const assertSelected = () => {
options.signal?.throwIfAborted();
options.assertCurrent?.();
const current = getRuntimeConfigSnapshot();
if (resolveConfiguredGitHubHost(current) !== host || resolveConfiguredGitHubApiBaseUrl(current) !== apiBaseUrl || gitHubPublicApi.GITHUB_API_BASE_URL !== apiBaseUrl) throw new gitHubPublicApi.ControlUiGitHubError(502, "GitHub host changed during project search");
};
assertSelected();
const normalizedQuery = query.trim().toLowerCase();
const { token, cacheScope } = options.token === void 0 ? gitHubPublicApi.resolveGitHubApiCredentialScope(options.env) : {
token: options.token,
cacheScope: gitHubPublicApi.githubApiCredentialCacheScope(options.token)
};
const cacheKey = `${normalizedQuery}\0${host}\0${apiBaseUrl}\0${cacheScope}`;
const now = options.now ?? Date.now();
const cached = searchCache.get(cacheKey);
const reusable = cached && cached.expiresAt > now && !cached.access.signal.aborted;
const entry = reusable ? cached : {
expiresAt: now + SEARCH_CACHE_MS,
access: createGitHubReadGroup(),
promise: Promise.resolve({
credential: "missing",
projects: []
})
};
const release = entry.access.add(assertSelected, options.signal);
if (!reusable) {
const fetchImpl = options.fetchImpl ?? fetch;
const identity = {
assertSelected: entry.access.assertCurrent,
revalidate: async () => entry.access.assertCurrent()
};
const request = (url, optionalAuth = true) => {
const readJson = async (requestToken) => gitHubPublicApi.readGitHubJsonResponse(await gitHubPublicApi.fetchGitHubApi(url, fetchImpl, requestToken, void 0, identity, void 0, entry.access.signal, void 0, apiBaseUrl));
return optionalAuth ? gitHubPublicApi.withOptionalGitHubAuth(token, readJson) : readJson(token);
};
entry.promise = searchProjectsUncached({
query: query.trim(),
request,
token
}).catch((error) => {
if (searchCache.get(cacheKey) === entry) searchCache.delete(cacheKey);
throw error;
});
}
searchCache.delete(cacheKey);
searchCache.set(cacheKey, entry);
pruneMapToMaxSize(searchCache, SEARCH_CACHE_LIMIT);
try {
return await racePromiseWithAbortSignal(entry.promise, options.signal);
} finally {
release();
assertSelected();
}
}
//#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
};
}
async 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 = await 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 projectCheckoutError(error) {
return errorShape(error instanceof ProjectCheckoutError ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, formatErrorMessage(error));
}
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).toSorted();
const results = probePaths.length ? await service.resolveRepositoryIdentities(probePaths) : [];
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);
const cfg = context.getRuntimeConfig();
const defaultRepository = configuredDefaultRepository(cfg);
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.");
};
try {
const registryProjects = await listProjectRegistry(cfg);
assertCurrent();
diagnostics?.mark("sessions");
const projects = registryProjects.map(sanitizeProjectRecord);
const canWrite = () => authorizeOperatorScopesForRequiredScope(WRITE_SCOPE, Array.isArray(client?.connect.scopes) ? client.connect.scopes : []).allowed;
const canCreateSession = () => authorizeOperatorScopesForMethod("sessions.create", Array.isArray(client?.connect.scopes) ? client.connect.scopes : []).allowed;
let store = {};
let observedProjects;
if (client?.authenticatedUserProfile?.profileId || params.includeObserved && canWrite()) {
if (params.includeObserved) store = (await loadCombinedSessionStoreForGatewayCoreAsync(cfg, { projection: "list" })).store;
else {
const projection = requireSessionRowProjection(context);
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();
}
diagnostics?.mark("recents");
const profileId = client?.authenticatedUserProfile?.profileId;
const recentProfileIds = profileId ? readCurrentUserProfileAliases(profileId) : void 0;
const recents = recentProfileIds ? await listProjectRecents(store, recentProfileIds, registryProjects) : void 0;
assertCurrent();
diagnostics?.mark("response");
assertCurrent();
const writable = canWrite();
const canCreate = canCreateSession();
respond(true, {
projects: writable ? projects : projects.map(({ id, displayName, source, agentId }) => agentId ? {
id,
displayName,
source,
agentId
} : {
id,
displayName,
source
}),
...canCreate ? { githubHost: resolveConfiguredGitHubHost(cfg) } : {},
...defaultRepository && (writable || canCreate) ? { defaultRepository } : {},
...recents ? { recents: writable ? recents : recents.filter((recent) => recent.kind === "project") } : {},
...writable && observedProjects ? { observedProjects } : {}
}, void 0);
} catch (error) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, formatErrorMessage(error)));
} finally {
diagnostics?.finish();
}
},
"projects.register": defineValidatedGatewayHandler("projects.register", validateProjectsRegisterParams, async ({ params, respond }) => {
respond(true, sanitizeProjectRecord(await registerProjectRegistry({
path: params.path,
name: params.name
})), void 0);
}, projectCheckoutError),
"projects.add": async ({ params, respond, context, signal }) => {
if (!assertValidParams(params, validateProjectsAddParams, "projects.add", respond)) return;
try {
const cfg = context.getRuntimeConfig();
respond(true, await materializeProjectClone({
cfg,
gitUrl: params.gitUrl,
name: params.name
}, {
signal,
token: githubApiToken(process.env, cfg)
}), 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": defineValidatedGatewayHandler("projects.searchRemote", validateProjectsSearchRemoteParams, async ({ params, respond, context, signal, hasCurrentClientAuthority }) => {
const cfg = context.getRuntimeConfig();
const host = resolveConfiguredGitHubHost(cfg);
const apiBaseUrl = resolveConfiguredGitHubApiBaseUrl(cfg);
const assertCurrent = () => {
signal?.throwIfAborted();
if (hasCurrentClientAuthority?.() === false) throw new Error("Project requester authority changed during search");
if (context.getRuntimeConfig() !== cfg) throw new gitHubPublicApi.ControlUiGitHubError(502, "GitHub host changed during project search");
};
assertCurrent();
const nativeToken = cfg.gateway?.projects?.nativeGitHubSearch === true ? await readCachedNativeGitHubToken(process.env) : void 0;
assertCurrent();
const result = await searchRemoteProjects(params.query, {
assertCurrent,
signal,
host,
apiBaseUrl,
...cfg.gateway?.projects?.nativeGitHubSearch === true ? { token: nativeToken ?? "" } : {}
});
assertCurrent();
respond(true, result, void 0);
}, (error) => {
const { message, ...details } = error instanceof gitHubPublicApi.ControlUiGitHubError || isTrustedSecretSurfaceUnavailableError(error) ? gitHubPublicApi.formatControlUiGitHubPreviewError(error) : {
message: "GitHub project search is unavailable. Retry shortly.",
retryable: true
};
return 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, projectCheckoutError(error));
return;
}
} else removed = await removeProjectRegistry(project);
if (!removed) {
respondUnknownProject();
return;
}
respond(true, { removed: true }, void 0);
}
};
}
const projectsHandlers = createProjectsHandlers(managedWorktrees);
//#endregion
export { projectsHandlers };