evOo-local/packages/adapters/src/computer-support.ts
cynarAI cf6ac7270e feat: rich project creation with folder and git wiring (#73)
Extend the existing NewSpaceDialog project flow so an optional host
folder becomes the space workspace root. When .git metadata is present,
remote URL, default branch, and head commit are stored on the space.

Team Computer file APIs expose the linked folder under project/ without
a second VCS stack. Desktop adds a native folder picker; web falls back
to an absolute path field.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-09-22 12:29:25 +02:00

187 lines
6.1 KiB
TypeScript

import { createHash } from "node:crypto";
import path from "node:path";
import type { ComputerAction, ComputerObservation, ComputerRef } from "@rakazo/adapter-kit";
import type { ComputerMode } from "@rakazo/contracts";
export function toComputerRef(computer: {
homeKey: string;
kind: string;
providerRef: string | null;
}): ComputerRef {
if (!computer.providerRef) throw new Error("computer provider reference is missing");
return {
id: computer.providerRef,
botId: computer.homeKey,
kind: computer.kind as ComputerRef["kind"],
providerRef: computer.providerRef,
};
}
const EMPTY_PNG = Uint8Array.from(
Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
"base64",
),
);
export function computerObservation(
image: Uint8Array,
input: Omit<ComputerObservation, "frameId" | "capturedAt" | "image">,
): ComputerObservation {
return {
...input,
image,
frameId: createHash("sha256").update(image).digest("hex"),
capturedAt: new Date().toISOString(),
};
}
export function placeholderObservation(label = "ready"): ComputerObservation {
return computerObservation(EMPTY_PNG, {
mimeType: "image/png",
width: 1,
height: 1,
activeWindow: { id: "placeholder", title: label },
});
}
export function applyPlaceholderAction(box: { screen: string }, action: ComputerAction): void {
if (action.kind === "clipboard") box.screen = action.text;
else if (action.kind === "open") box.screen = `opened ${action.path}`;
else if (action.kind === "launch") box.screen = `launched ${action.application}`;
else box.screen = action.kind;
}
export function boundedComputerActions(actions: readonly ComputerAction[]): ComputerAction[] {
if (actions.length > 24) throw new Error("computer action batch exceeds 24 actions");
return [...actions];
}
export function clampRounded(value: number, min: number, max: number): number {
return Math.min(Math.max(Math.round(value), min), max);
}
export function normalizeWorkspacePath(value: string): string {
const normalized = value.replace(/\\/g, "/").replace(/^\/+/, "");
const rawSegments = normalized.split("/").filter(Boolean);
if (rawSegments.some((segment) => segment === "..")) {
throw new Error("Path escapes the computer workspace");
}
const segments = rawSegments.filter((segment) => segment !== ".");
return segments.join("/");
}
export function workspacePath(root: string, relative: string): string {
const normalized = normalizeWorkspacePath(relative);
return normalized ? path.posix.join(root, normalized) : root;
}
export function teamBotWorkspaceDirectory(botId: string): string {
return path.posix.join("bots", normalizeWorkspacePath(botId));
}
const TEAM_WORKSPACE_ESCAPE = "Path escapes the bot computer workspace";
/** Bot home segment under `bots/<botId>/…`, or null for shared/root paths. */
export function teamWorkspaceTargetBotId(storedPath: string): string | null {
const normalized = normalizeWorkspacePath(storedPath);
if (normalized === "bots" || !normalized.startsWith("bots/")) return null;
const remainder = normalized.slice("bots/".length);
if (!remainder) return null;
const [botId] = remainder.split("/");
return botId || null;
}
export function assertTeamWorkspacePathAllowed(
storedPath: string,
allowedBotIds: ReadonlySet<string>,
): void {
const targetBotId = teamWorkspaceTargetBotId(storedPath);
if (!targetBotId || allowedBotIds.has(targetBotId)) return;
throw new Error(TEAM_WORKSPACE_ESCAPE);
}
export function assertTeamShellCommandAllowed(
command: string,
allowedBotIds: ReadonlySet<string>,
): void {
for (const match of command.matchAll(/\bbots\/([A-Za-z0-9_-]+)/g)) {
const botId = match[1];
if (botId && !allowedBotIds.has(botId)) {
throw new Error(TEAM_WORKSPACE_ESCAPE);
}
}
}
export function filterTeamWorkspaceEntries<T extends { path: string }>(
entries: readonly T[],
allowedBotIds: ReadonlySet<string>,
): T[] {
return entries.filter((entry) => {
const targetBotId = teamWorkspaceTargetBotId(entry.path);
return !targetBotId || allowedBotIds.has(targetBotId);
});
}
export function resolveBotWorkspacePath(
scope: ComputerMode,
botId: string,
requestedPath: string,
): string {
if (scope !== "team") return requestedPath;
const explicitRootPath = stripVirtualWorkspaceRoot(requestedPath);
const normalized = normalizeWorkspacePath(explicitRootPath ?? requestedPath);
if (explicitRootPath !== null || isTeamRootPath(normalized)) return normalized;
return path.posix.join(teamBotWorkspaceDirectory(botId), normalized);
}
export function resolveBotWorkspaceCwd(
scope: ComputerMode,
botId: string,
requestedCwd: string | undefined,
): string | undefined {
if (scope !== "team") return requestedCwd;
if (!requestedCwd || requestedCwd === ".") return teamBotWorkspaceDirectory(botId);
if (requestedCwd.startsWith("/")) return requestedCwd;
return resolveBotWorkspacePath(scope, botId, requestedCwd);
}
export function displayBotWorkspacePath(
scope: ComputerMode,
botId: string,
requestedPath: string,
storedPath: string,
): string {
if (scope !== "team" || !isBotRelativeRequest(requestedPath)) return storedPath;
const normalized = normalizeWorkspacePath(storedPath);
const botDirectory = teamBotWorkspaceDirectory(botId);
if (normalized === botDirectory) return "";
return normalized.startsWith(`${botDirectory}/`)
? normalized.slice(botDirectory.length + 1)
: normalized;
}
function isBotRelativeRequest(value: string): boolean {
if (stripVirtualWorkspaceRoot(value) !== null) return false;
return !isTeamRootPath(normalizeWorkspacePath(value));
}
function isTeamRootPath(value: string): boolean {
return (
value === "shared" ||
value.startsWith("shared/") ||
value === "project" ||
value.startsWith("project/") ||
value === "bots" ||
value.startsWith("bots/")
);
}
function stripVirtualWorkspaceRoot(value: string): string | null {
const portable = value.replace(/\\/g, "/");
if (portable === "/") return "";
return portable.startsWith("/") ? portable.slice(1) : null;
}
export { shellQuote } from "@rakazo/core/node/desktop-runtime";