import fs from "node:fs"; import os from "node:os"; import path from "node:path"; const DEFAULT_INSTANCE_ID = "default"; const INSTANCE_ID_RE = /^[a-zA-Z0-9_-]+$/; const PATH_SEGMENT_RE = /^[a-zA-Z0-9_-]+$/; const FRIENDLY_PATH_SEGMENT_RE = /[^a-zA-Z0-9._-]+/g; function expandHomePrefix(value: string): string { if (value === "~") return os.homedir(); if (value.startsWith("~/")) return path.resolve(os.homedir(), value.slice(2)); return value; } // [nexus] Read ~/.nexus pointer file for custom home directory function resolveNexusPointerFile(): string | null { const pointerPath = path.resolve(os.homedir(), ".nexus"); try { const raw = fs.readFileSync(pointerPath, "utf-8").trim(); if (raw.length > 0) { return path.resolve(expandHomePrefix(raw)); } } catch { // ~/.nexus does not exist or is unreadable — fall through } return null; } export function resolvePaperclipHomeDir(): string { // [nexus] Pointer-file: ~/.nexus overrides all other home resolution const nexusRoot = resolveNexusPointerFile(); if (nexusRoot) return nexusRoot; const envHome = process.env.PAPERCLIP_HOME?.trim(); if (envHome) return path.resolve(expandHomePrefix(envHome)); return path.resolve(os.homedir(), ".paperclip"); } export function resolvePaperclipInstanceId(): string { const raw = process.env.PAPERCLIP_INSTANCE_ID?.trim() || DEFAULT_INSTANCE_ID; if (!INSTANCE_ID_RE.test(raw)) { throw new Error(`Invalid PAPERCLIP_INSTANCE_ID '${raw}'.`); } return raw; } export function resolvePaperclipInstanceRoot(): string { return path.resolve(resolvePaperclipHomeDir(), "instances", resolvePaperclipInstanceId()); } export function resolveDefaultConfigPath(): string { return path.resolve(resolvePaperclipInstanceRoot(), "config.json"); } export function resolveDefaultEmbeddedPostgresDir(): string { return path.resolve(resolvePaperclipInstanceRoot(), "db"); } export function resolveDefaultLogsDir(): string { return path.resolve(resolvePaperclipInstanceRoot(), "logs"); } export function resolveDefaultSecretsKeyFilePath(): string { return path.resolve(resolvePaperclipInstanceRoot(), "secrets", "master.key"); } export function resolveDefaultStorageDir(): string { return path.resolve(resolvePaperclipInstanceRoot(), "data", "storage"); } export function resolveDefaultBackupDir(): string { return path.resolve(resolvePaperclipInstanceRoot(), "data", "backups"); } // [nexus] Accept agent object for human-readable slugified workspace dirs export function resolveDefaultAgentWorkspaceDir(agent: { id: string; name?: string | null }): string { // Use slugified name for human-readable dirs; fall back to sanitized id const segment = agent.name?.trim() ? sanitizeFriendlyPathSegment(agent.name, agent.id) : sanitizeFriendlyPathSegment(agent.id, agent.id); return path.resolve(resolvePaperclipInstanceRoot(), "workspaces", segment); } function sanitizeFriendlyPathSegment(value: string | null | undefined, fallback = "_default"): string { const trimmed = value?.trim() ?? ""; if (!trimmed) return fallback; const sanitized = trimmed .replace(FRIENDLY_PATH_SEGMENT_RE, "-") .replace(/^-+|-+$/g, ""); return sanitized || fallback; } export function resolveManagedProjectWorkspaceDir(input: { companyId: string; projectId: string; repoName?: string | null; }): string { const companyId = input.companyId.trim(); const projectId = input.projectId.trim(); if (!companyId || !projectId) { throw new Error("Managed project workspace path requires companyId and projectId."); } return path.resolve( resolvePaperclipInstanceRoot(), "projects", sanitizeFriendlyPathSegment(companyId, "company"), sanitizeFriendlyPathSegment(projectId, "project"), sanitizeFriendlyPathSegment(input.repoName, "_default"), ); } export function resolveHomeAwarePath(value: string): string { return path.resolve(expandHomePrefix(value)); } // [nexus] Skill registry paths export function resolveSkillRegistryDbPath(): string { return path.resolve(resolvePaperclipInstanceRoot(), "skills", "registry.db"); } export function resolveSkillCacheDir(skillId: string, versionId: string): string { return path.resolve(resolvePaperclipInstanceRoot(), "skills", "cache", skillId, versionId); }