Ctrl K
ComunidadGuías…

server/bot-cwd.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { lstatSync, realpathSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { isAbsolute, relative, resolve } from "node:path";

import { isOwnThreadFolder } from "../shared/thread-folder.ts";

export type CwdValidation = { ok: true; cwd: string | null } | { ok: false; error: string };

export function validateBotCwd(input: unknown): CwdValidation {
  if (input === null) return { ok: true, cwd: null };
  if (typeof input !== "string") return { ok: false, error: "working folder must be a path" };
  const trimmed = input.trim();
  if (!trimmed) return { ok: true, cwd: null };
  const expanded = trimmed === "~" || trimmed.startsWith("~/") ? homedir() + trimmed.slice(1) : trimmed;
  if (!isAbsolute(expanded)) return { ok: false, error: "working folder must be an absolute path" };
  const cwd = resolve(expanded);
  let stat;
  try {
    stat = statSync(cwd);
  } catch {
    return { ok: false, error: `that folder doesn't exist: ${cwd}` };
  }
  if (!stat.isDirectory()) return { ok: false, error: `that path is not a folder: ${cwd}` };
  return { ok: true, cwd };
}

/** The working folder a thread moves to on this turn: its bot's, while the thread is still in its own folder. Never for a
 * conversation that keeps a private folder (a Cloud guest's), nor for an engine that works in no folder. */
export function folderToFollow(bot: { id: string; cwd?: string }, threadId: string, threadCwd: string | null | undefined,
  opts: { worksInFolders: boolean; keepsPrivateFolder: boolean }): string | undefined {
  if (!bot.cwd || !opts.worksInFolders || opts.keepsPrivateFolder) return undefined;
  return isOwnThreadFolder(threadCwd, bot.id, threadId) ? bot.cwd : undefined;
}

/** A conversation's folder a teammate may be sent to: one that still exists, outside OpenMausBot's own data (a bot's private workspace).
 * The path as given and where it really leads are both checked against the data root as it really resolves too, and a
 * link is shared as the folder it resolved to at check time, so retargeting it afterwards cannot move a shared task. */
export function sharedProjectFolder(cwd: string | null | undefined, dataDir: string): string | undefined {
  const checked = cwd ? validateBotCwd(cwd) : undefined;
  if (!checked?.ok || !checked.cwd) return undefined;
  let real: string;
  let root: string;
  try {
    real = realpathSync(checked.cwd);
    root = realpathSync(resolve(dataDir));
  } catch {
    return undefined;
  }
  for (const candidate of [checked.cwd, real]) {
    const inside = relative(root, candidate);
    if (inside === "" || (!inside.startsWith("..") && !isAbsolute(inside))) return undefined;
  }
  return lstatSync(checked.cwd).isSymbolicLink() ? real : checked.cwd;
}

/** coordinate_bots' in_my_folder: the sender's project folder for the teammate threads it opens outside a room. On unless
 * false; with no folder to share, or in a room (whose folder governs there), the hand-off goes ahead without one. */
export function inMyFolderCwd(wanted: boolean | undefined, outsideRoom: boolean, senderCwd: string | null | undefined, dataDir: string): { cwd?: string } {
  if (wanted === false || !outsideRoom) return {};
  const cwd = sharedProjectFolder(senderCwd, dataDir);
  return cwd ? { cwd } : {};
}