import * as os from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; /** * Centralized path resolver for ConnectAI. * * Why this module exists: * - Brain / agent-skills / workspace paths are read from many places (utils, sidebar, * bridge, agent). Embedding the same `~`-expansion + abs-path-only check in each * call site makes them drift over time. * - New external integrations (skill-inject, future detached-company mode) need a * single source of truth so they can't accidentally write outside the sandboxed * user folders. * * Conventions: * - All exported functions return absolute, normalized paths (or empty string if * the user has not configured a value AND no fallback exists). * - Relative-path inputs are silently rejected (returned as empty) to avoid * surprising writes inside random workspaces. * - This module never throws and never creates directories — callers ensure * existence on their own (`fs.mkdirSync(..., { recursive: true })`). */ /** Expand a leading `~` / `~/` to the user's home directory. Pure function. */ export function expandTilde(raw: string): string { const trimmed = (raw || '').trim(); if (!trimmed) return ''; if (trimmed === '~') return os.homedir(); if (trimmed.startsWith('~/')) return path.join(os.homedir(), trimmed.slice(2)); return trimmed; } /** * Normalize a user-supplied path string. Returns an empty string for any input * that is empty, blank, or non-absolute after `~` expansion. Relative paths are * intentionally rejected — see module header. */ export function resolvePathInput(raw: string): string { const expanded = expandTilde(raw); if (!expanded) return ''; if (!path.isAbsolute(expanded)) return ''; return path.normalize(expanded); } /** * Best-effort read of a string config value. Returns empty string when VS Code * config is unavailable (e.g. unit tests not mocking workspace) so callers can * fall through to defaults without try/catch noise. */ function _safeGetConfigString(section: string, key: string): string { try { return vscode.workspace.getConfiguration(section).get(key, '') || ''; } catch { return ''; } } /** * Active brain directory. * * Resolution order: * 1. VS Code config `g1nation.localBrainPath` (after `~` + abs-path normalization). * 2. The first configured brain profile's `localBrainPath` (handled by callers). * 3. Empty string — caller decides on a default (utils.ts already has the * profile-aware logic; this function is only for the simple-path case). * * Note: this intentionally does NOT consult `g1nation.brainProfiles` — the * profile-aware resolver lives in [src/utils.ts](../utils.ts) (`_getBrainDir`) * and depends on the active-brain selection. Use this function only when you * need a plain folder path without profile semantics (e.g. external HTTP * endpoints injecting into the user's primary brain). */ export function resolveBrainDirFromConfig(): string { const raw = _safeGetConfigString('g1nation', 'localBrainPath'); return resolvePathInput(raw); } /** * Resolve the agent-skills directory used by `[.agent/skills/*.md]` markdown * skill files (the per-workspace agent skill bank that the sidebar's * `_sendAgentsList` and `_createAgent` operate on). * * Resolution order: * 1. The first VS Code workspace folder + `/.agent/skills/` (creating the * folder is the caller's responsibility). * 2. Empty string when no workspace is open — callers must short-circuit. * * The legacy default `E:\Wiki\Agent\.agent\skills` from sidebarProvider.ts is * preserved as a fall-through hint for the original author's machine. */ export function resolveAgentSkillsDir(): string { const legacy = 'E:\\Wiki\\Agent\\.agent\\skills'; try { const fs = require('fs') as typeof import('fs'); if (fs.existsSync(legacy)) return legacy; } catch { /* fs unavailable in some isolated tests */ } const folders = vscode.workspace.workspaceFolders; if (folders && folders.length > 0) { return path.join(folders[0].uri.fsPath, '.agent', 'skills'); } return ''; } /** * Returns true iff `child` is the same as `parent` or a descendant of it * (after path normalization). Used to harden file writes against `..` traversal. * * Both paths must be absolute. */ export function isInside(parent: string, child: string): boolean { if (!parent || !child) return false; const p = path.resolve(parent); const c = path.resolve(child); if (c === p) return true; return c.startsWith(p + path.sep); } /** * Pick the best `ConfigurationTarget` to write a key to: write into whichever * scope already holds the value, falling back to Global. * * Why this matters: VS Code's `getConfiguration().get(key)` resolves through * Folder → Workspace → User → default. If a Workspace value is set and we * blindly write to Global, every subsequent read keeps returning the stale * Workspace value — which is exactly the "sidebar shows e2b but Settings * shows e4b" bug. * * Returns the section's effective inspect record alongside the target so * callers can debug or surface conflicts to the user. */ export function pickConfigTarget(section: string, key: string): { target: vscode.ConfigurationTarget; inspect: ReturnType; } { const cfg = vscode.workspace.getConfiguration(section); const inspect = cfg.inspect(key); if (inspect?.workspaceFolderValue !== undefined) { return { target: vscode.ConfigurationTarget.WorkspaceFolder, inspect }; } if (inspect?.workspaceValue !== undefined) { return { target: vscode.ConfigurationTarget.Workspace, inspect }; } return { target: vscode.ConfigurationTarget.Global, inspect }; }