import { lstatSync, readFileSync, realpathSync, statSync, readdirSync } from "node:fs"; import type { IncomingMessage, ServerResponse } from "node:http"; import { homedir } from "node:os"; import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; import { CONFIG_DIR_NAME, DefaultPackageManager, SettingsManager, getAgentDir, loadSkills, type PackageSource, type ResolvedResource, type Skill, } from "@earendil-works/pi-coding-agent"; import { completeHostPath, createHostWorkspaceDirectory, discoverProjects, getHostPathStatus, HostPathCreateError, scanDirectories, } from "../host.js"; import { listConfiguredHostExtensionResources } from "../extension-loader.js"; import { resolveSdkSessionCwd } from "../sdk-backend.js"; import type { RouteContext, RouteDispatcher, RouteHelpers } from "./types.js"; const MAX_SKILL_FILE_BYTES = 1024 * 1024; const MAX_SKILL_LISTED_FILES = 500; interface SkillRouteInfo { name: string; description: string; path: string; builtIn: boolean; enabled: boolean; } type PiResourceType = "skills" | "extensions"; type PiResourceScope = "user" | "project"; class WorkspaceResourceScopeError extends Error { constructor( message: string, readonly status: number, ) { super(message); this.name = "WorkspaceResourceScopeError"; } } class PiSettingsOperationError extends Error { constructor(operation: string, detail: string) { super(`Pi settings ${operation} failed: ${detail}`); this.name = "PiSettingsOperationError"; } } class PiResourceNotFoundError extends Error { constructor(message = "Pi resource not found for cwd") { super(message); this.name = "PiResourceNotFoundError"; } } function httpStatusForPiResourceError(err: unknown): number { if (err instanceof WorkspaceResourceScopeError) return err.status; if (err instanceof PiSettingsOperationError) return 500; if (err instanceof PiResourceNotFoundError) return 404; // extension-loader and other Pi helpers throw plain Errors with this prefix. if (err instanceof Error && err.message.startsWith("Pi settings ")) return 500; return 400; } function errorMessage(err: unknown, fallback: string): string { return err instanceof Error ? err.message : fallback; } function resolveResourceCwd(cwd: string | undefined): string { const raw = cwd?.trim(); if (!raw) return homedir(); const expanded = raw === "~" || raw.startsWith("~/") ? raw.replace(/^~(?=\/|$)/, homedir()) : raw; return resolve(expanded); } function sdkSkillToInfo(skill: Skill, resource?: ResolvedResource): SkillRouteInfo { return { name: skill.name, description: skill.description, path: skill.baseDir, builtIn: false, enabled: resource?.enabled ?? true, }; } function skillMatchesResource(skill: Skill, resource: ResolvedResource): boolean { return ( resource.path === skill.filePath || resource.path === skill.baseDir || skill.filePath.startsWith(`${resource.path}/`) || (resource.path.endsWith("/SKILL.md") && dirname(resource.path) === skill.baseDir) ); } async function resolvePiResources(cwd: string): Promise<{ agentDir: string; settingsManager: SettingsManager; resolved: Awaited>; }> { const agentDir = getAgentDir(); const settingsManager = SettingsManager.create(cwd, agentDir); throwIfPiSettingsErrors(settingsManager, "load"); const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager }); const resolved = await packageManager.resolve(async () => "skip"); throwIfPiSettingsErrors(settingsManager, "resolve"); return { agentDir, settingsManager, resolved }; } function throwIfPiSettingsErrors(settingsManager: SettingsManager, operation: string): void { const errors = settingsManager.drainErrors(); if (errors.length === 0) return; const detail = errors .slice(0, 5) .map((entry) => `${entry.scope}: ${entry.error.message}`) .join("; "); throw new PiSettingsOperationError(operation, detail); } async function listConfiguredHostSkills(cwd: string): Promise { const { agentDir, resolved } = await resolvePiResources(cwd); const skillPaths = resolved.skills.map((resource) => resource.path); const result = loadSkills({ cwd, agentDir, skillPaths, includeDefaults: false, }); return result.skills.map((skill) => sdkSkillToInfo( skill, resolved.skills.find((resource) => skillMatchesResource(skill, resource)), ), ); } function isWithinDirectory(path: string, baseDir: string): boolean { const rel = relative(baseDir, path); return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel)); } function safeRealSkillBase(skillPath: string): string | undefined { try { if (!lstatSync(skillPath).isDirectory()) { return undefined; } return realpathSync(skillPath); } catch { return undefined; } } function listFilesRecursive( baseDir: string, prefix = "", realBase = safeRealSkillBase(baseDir), count = { value: 0 }, ): string[] { if (!realBase || count.value >= MAX_SKILL_LISTED_FILES) return []; const skipDirs = new Set([ "__pycache__", "node_modules", ".git", ".venv", ".mypy_cache", ".pytest_cache", ".ruff_cache", "__pypackages__", ]); const skipExts = new Set([".pyc", ".pyo", ".o", ".so", ".dylib"]); const dir = prefix ? join(baseDir, prefix) : baseDir; let entries: string[]; try { entries = readdirSync(dir); } catch { return []; } const results: string[] = []; for (const entry of entries.sort()) { if (count.value >= MAX_SKILL_LISTED_FILES) break; const rel = prefix ? `${prefix}/${entry}` : entry; const full = join(dir, entry); try { const stat = lstatSync(full); if (stat.isSymbolicLink()) { continue; } const real = realpathSync(full); if (!isWithinDirectory(real, realBase)) { continue; } if (stat.isDirectory()) { if (!skipDirs.has(entry)) { results.push(...listFilesRecursive(baseDir, rel, realBase, count)); } } else if (stat.isFile()) { const ext = entry.includes(".") ? entry.substring(entry.lastIndexOf(".")) : ""; if (!skipExts.has(ext)) { results.push(rel); count.value += 1; } } } catch { // Ignore unreadable entries. } } return results; } function resolveSkillFilePath(skillPath: string, relPath: string): string | undefined { const realBase = safeRealSkillBase(skillPath); if (!realBase) return undefined; const requested = resolve(skillPath, relPath); try { if (lstatSync(requested).isSymbolicLink()) { return undefined; } const resolved = realpathSync(requested); if (!isWithinDirectory(resolved, realBase)) { return undefined; } const stat = statSync(resolved); if (!stat.isFile() || stat.size > MAX_SKILL_FILE_BYTES) { return undefined; } return resolved; } catch { return undefined; } } function readSkillFileContent(skillPath: string, relPath: string): string | undefined { const target = resolveSkillFilePath(skillPath, relPath); if (!target) return undefined; try { return readFileSync(target, "utf-8"); } catch { return undefined; } } function toPosixPath(path: string): string { return path.split(sep).join("/"); } function stripPatternPrefix(pattern: string): string { return pattern.startsWith("!") || pattern.startsWith("+") || pattern.startsWith("-") ? pattern.slice(1) : pattern; } function projectSettingsBaseDir(cwd: string): string { return join(cwd, CONFIG_DIR_NAME); } function settingsBaseDir(scope: PiResourceScope, cwd: string, agentDir: string): string { return scope === "project" ? projectSettingsBaseDir(cwd) : agentDir; } function topLevelBaseDir(resource: ResolvedResource, cwd: string, agentDir: string): string { if (resource.metadata.baseDir) return resource.metadata.baseDir; return resource.metadata.scope === "project" ? projectSettingsBaseDir(cwd) : agentDir; } function topLevelSettingsBaseDir( targetScope: PiResourceScope, resource: ResolvedResource, cwd: string, agentDir: string, ): string { if (targetScope === "project") return projectSettingsBaseDir(cwd); return resource.metadata.baseDir ?? agentDir; } function resourcePattern(resource: ResolvedResource, cwd: string, agentDir: string): string { const baseDir = resource.metadata.origin === "package" ? (resource.metadata.baseDir ?? dirname(resource.path)) : topLevelBaseDir(resource, cwd, agentDir); return toPosixPath(relative(baseDir, resource.path)); } function isLocalPackageSource(source: string): boolean { const trimmed = source.trim(); return !( trimmed.startsWith("npm:") || trimmed.startsWith("git:") || trimmed.startsWith("github:") || trimmed.startsWith("http:") || trimmed.startsWith("https:") || trimmed.startsWith("ssh:") ); } function packageSourcesMatch( leftSource: string, leftScope: PiResourceScope, rightSource: string, rightScope: PiResourceScope, cwd: string, agentDir: string, ): boolean { if (leftSource === rightSource) return true; if (!isLocalPackageSource(leftSource) || !isLocalPackageSource(rightSource)) { return false; } return ( resolveSettingsPath(leftSource, settingsBaseDir(leftScope, cwd, agentDir)) === resolveSettingsPath(rightSource, settingsBaseDir(rightScope, cwd, agentDir)) ); } function packageSourceForSettingsScope( resource: ResolvedResource, targetScope: PiResourceScope, cwd: string, agentDir: string, ): string { if (targetScope === resource.metadata.scope) { return resource.metadata.source; } const source = resource.metadata.source; if (!isLocalPackageSource(source)) { return source; } if (resource.metadata.scope !== "user" && resource.metadata.scope !== "project") { return source; } // Pi TUI project deltas store local package sources relative to /.pi. const resolvedSource = resolveSettingsPath( source, settingsBaseDir(resource.metadata.scope, cwd, agentDir), ); if (targetScope === "project") { return toPosixPath(relative(projectSettingsBaseDir(cwd), resolvedSource)) || "."; } return resolvedSource; } function topLevelResourcePattern( resource: ResolvedResource, targetScope: PiResourceScope, cwd: string, agentDir: string, ): string { return toPosixPath( relative(topLevelSettingsBaseDir(targetScope, resource, cwd, agentDir), resource.path), ); } function expandSettingsPathPattern(pattern: string): string { return pattern === "~" || pattern.startsWith("~/") ? pattern.replace(/^~(?=\/|$)/, homedir()) : pattern; } function resolveSettingsPath(path: string, baseDir: string): string { const expanded = expandSettingsPathPattern(path); return resolve(isAbsolute(expanded) ? expanded : join(baseDir, expanded)).replace(/\/+$/, ""); } function resolveSettingsEntryPath(entry: string, baseDir: string): string { return resolveSettingsPath(stripPatternPrefix(entry), baseDir); } function settingsEntryMatchesResource( resourceType: PiResourceType, entry: string, resource: ResolvedResource, baseDir: string, ): boolean { const entryPath = resolveSettingsEntryPath(entry, baseDir); const resourcePath = resolve(resource.path).replace(/\/+$/, ""); if (entryPath === resourcePath) return true; if (resourceType === "skills") { return basename(resourcePath) === "SKILL.md" && dirname(resourcePath) === entryPath; } return ( (basename(resourcePath) === "index.ts" || basename(resourcePath) === "index.js") && dirname(resourcePath) === entryPath ); } function writeTopLevelResourceSettings( settingsManager: SettingsManager, resource: ResolvedResource, resourceType: PiResourceType, cwd: string, agentDir: string, enabled: boolean, targetScope: PiResourceScope, keepResourceEntry: boolean, ): void { const settings = targetScope === "project" ? settingsManager.getProjectSettings() : settingsManager.getGlobalSettings(); const baseDir = topLevelSettingsBaseDir(targetScope, resource, cwd, agentDir); const pattern = topLevelResourcePattern(resource, targetScope, cwd, agentDir); const updated = ([...(settings[resourceType] ?? [])] as string[]).filter( (entry) => !settingsEntryMatchesResource(resourceType, entry, resource, baseDir), ); if (keepResourceEntry) { updated.push(pattern); } updated.push(`${enabled ? "+" : "-"}${pattern}`); if (targetScope === "project") { if (resourceType === "extensions") { settingsManager.setProjectExtensionPaths(updated); } else { settingsManager.setProjectSkillPaths(updated); } } else if (resourceType === "extensions") { settingsManager.setExtensionPaths(updated); } else { settingsManager.setSkillPaths(updated); } } function writeResourceSettings( settingsManager: SettingsManager, resource: ResolvedResource, resourceType: PiResourceType, cwd: string, agentDir: string, enabled: boolean, targetScope: PiResourceScope = resource.metadata.scope as PiResourceScope, ): void { if (targetScope !== "user" && targetScope !== "project") { throw new Error("Temporary resources cannot be edited from Oppi"); } // Pi force-enable/disable patterns apply only to resources already present in // that settings scope. When a workspace overrides a user/global resource, add // the resource path to project settings before the +/- override so project // precedence can shadow the global auto-discovered entry. const keepResourceEntry = targetScope !== resource.metadata.scope || resource.metadata.source === "local"; writeTopLevelResourceSettings( settingsManager, resource, resourceType, cwd, agentDir, enabled, targetScope, keepResourceEntry, ); } function writePackageResourceSettings( settingsManager: SettingsManager, resource: ResolvedResource, resourceType: PiResourceType, cwd: string, agentDir: string, enabled: boolean, targetScope: PiResourceScope = resource.metadata.scope as PiResourceScope, ): void { if (targetScope !== "user" && targetScope !== "project") { throw new Error("Temporary resources cannot be edited from Oppi"); } const settings = targetScope === "project" ? settingsManager.getProjectSettings() : settingsManager.getGlobalSettings(); const packages = [...(settings.packages ?? [])] as PackageSource[]; const packageSource = packageSourceForSettingsScope(resource, targetScope, cwd, agentDir); const resourceScope = resource.metadata.scope === "project" || resource.metadata.scope === "user" ? resource.metadata.scope : targetScope; let packageIndex = packages.findIndex((pkg) => { const source = typeof pkg === "string" ? pkg : pkg.source; return ( packageSourcesMatch(source, targetScope, packageSource, targetScope, cwd, agentDir) || packageSourcesMatch( source, targetScope, resource.metadata.source, resourceScope, cwd, agentDir, ) ); }); // Cross-scope project overrides must be Pi TUI-shaped package deltas // (`autoload: false`) so they layer over the global package instead of // replacing it and dropping unrelated package resources. const isProjectOverride = targetScope === "project" && targetScope !== resource.metadata.scope; if (packageIndex < 0) { if (targetScope === resource.metadata.scope) { throw new Error(`Package source missing from ${targetScope} settings`); } packages.push( isProjectOverride ? { source: packageSource, autoload: false } : { source: packageSource }, ); packageIndex = packages.length - 1; } let pkg = packages[packageIndex]; if (typeof pkg === "string") { pkg = isProjectOverride ? { source: pkg, autoload: false } : { source: pkg }; packages[packageIndex] = pkg; } else if (isProjectOverride && pkg.autoload !== false) { pkg = { ...pkg, autoload: false }; packages[packageIndex] = pkg; } // Toggle always writes a +/- filter entry, so the package object retains filters. const pattern = resourcePattern(resource, cwd, agentDir); const current = [...(pkg[resourceType] ?? [])]; pkg[resourceType] = current .filter((entry) => stripPatternPrefix(entry) !== pattern) .concat(`${enabled ? "+" : "-"}${pattern}`); if (targetScope === "project") { settingsManager.setProjectPackages(packages); } else { settingsManager.setPackages(packages); } } function normalizeClientResourcePath(path: string): string { const trimmed = path.trim().replace(/\/+$/, ""); const expanded = trimmed === "~" || trimmed.startsWith("~/") ? trimmed.replace(/^~(?=\/|$)/, homedir()) : trimmed; return resolve(expanded); } function resourceMatchesClientPath( type: PiResourceType, resource: ResolvedResource, requestedPath: string, ): boolean { const resourcePath = resolve(resource.path).replace(/\/+$/, ""); if (resourcePath === requestedPath) { return true; } if (type === "skills") { return basename(resourcePath) === "SKILL.md" && dirname(resourcePath) === requestedPath; } return ( (basename(resourcePath) === "index.ts" || basename(resourcePath) === "index.js") && dirname(resourcePath) === requestedPath ); } async function setPiResourceEnabled(options: { cwd: string; type: PiResourceType; path: string; enabled: boolean; preferProjectScope?: boolean; }): Promise { const { agentDir, settingsManager, resolved } = await resolvePiResources(options.cwd); const resources = resolved[options.type]; const requestedPath = normalizeClientResourcePath(options.path); const resource = resources.find((item) => resourceMatchesClientPath(options.type, item, requestedPath), ); if (!resource) { throw new PiResourceNotFoundError(); } const targetScope = options.preferProjectScope ? "project" : resource.metadata.scope; if (targetScope !== "user" && targetScope !== "project") { throw new Error("Temporary resources cannot be edited from Oppi"); } if (resource.metadata.origin === "package") { writePackageResourceSettings( settingsManager, resource, options.type, options.cwd, agentDir, options.enabled, targetScope, ); } else { writeResourceSettings( settingsManager, resource, options.type, options.cwd, agentDir, options.enabled, targetScope, ); } await finishPiSettingsWrite(settingsManager); } async function finishPiSettingsWrite(settingsManager: SettingsManager): Promise { await settingsManager.flush(); throwIfPiSettingsErrors(settingsManager, "write"); } export function createSkillRoutes(ctx: RouteContext, helpers: RouteHelpers): RouteDispatcher { function resolveScopedResourceCwd(options: { workspaceId?: string; cwd?: string }): { cwd?: string; workspaceScoped: boolean; } { if (options.workspaceId === undefined) { return { cwd: options.cwd, workspaceScoped: false }; } const workspaceId = options.workspaceId.trim(); if (!workspaceId) { throw new WorkspaceResourceScopeError("workspaceId must not be empty", 400); } const workspace = ctx.storage.getWorkspace(workspaceId); if (!workspace) { throw new WorkspaceResourceScopeError("Workspace not found", 404); } return { cwd: resolveSdkSessionCwd(workspace, undefined, { dataDir: ctx.storage.getDataDir() }), workspaceScoped: true, }; } async function handleListSkills(url: URL, res: ServerResponse): Promise { try { const { cwd } = resolveScopedResourceCwd({ workspaceId: url.searchParams.get("workspaceId") ?? undefined, cwd: url.searchParams.get("cwd") ?? undefined, }); if (cwd) { helpers.json(res, { skills: await listConfiguredHostSkills(cwd) }); return; } helpers.json(res, { skills: ctx.skillRegistry.list() }); } catch (err: unknown) { helpers.error( res, httpStatusForPiResourceError(err), errorMessage(err, "Failed to resolve Pi skills for cwd"), ); } } function handleRescanSkills(res: ServerResponse): void { const event = ctx.skillRegistry.scan(); helpers.json(res, { skills: ctx.skillRegistry.list(), changed: event }); } async function handleListExtensions(url: URL, res: ServerResponse): Promise { try { const { cwd } = resolveScopedResourceCwd({ workspaceId: url.searchParams.get("workspaceId") ?? undefined, cwd: url.searchParams.get("cwd") ?? undefined, }); // listFromResolvedResources already dedupes by extension name. const extensions = ( await listConfiguredHostExtensionResources({ cwd, agentDir: getAgentDir() }) ).map((ext) => ({ ...ext, enabled: ext.enabled ?? true, source: "pi" as const, })); helpers.json(res, { extensions }); } catch (err: unknown) { helpers.error( res, httpStatusForPiResourceError(err), errorMessage(err, "Failed to resolve Pi extensions for cwd"), ); } } async function handleGetSkillDetail(name: string, url: URL, res: ServerResponse): Promise { try { const cwd = url.searchParams.get("cwd") ?? undefined; if (cwd) { const skill = (await listConfiguredHostSkills(cwd)).find((item) => item.name === name); if (!skill) { helpers.error(res, 404, "Skill not found"); return; } const content = readSkillFileContent(skill.path, "SKILL.md") ?? ""; helpers.json(res, { skill, content, files: listFilesRecursive(skill.path) }); return; } const detail = ctx.skillRegistry.getDetail(name); if (!detail) { helpers.error(res, 404, "Skill not found"); return; } helpers.json(res, detail); } catch (err: unknown) { helpers.error( res, httpStatusForPiResourceError(err), errorMessage(err, "Failed to load skill detail"), ); } } async function handleGetSkillFile(name: string, url: URL, res: ServerResponse): Promise { const filePath = url.searchParams.get("path"); if (!filePath) { helpers.error(res, 400, "path parameter required"); return; } try { const cwd = url.searchParams.get("cwd") ?? undefined; if (cwd) { const skill = (await listConfiguredHostSkills(cwd)).find((item) => item.name === name); const content = skill ? readSkillFileContent(skill.path, filePath) : undefined; if (content === undefined) { helpers.error(res, 404, "File not found"); return; } helpers.json(res, { content }); return; } const content = ctx.skillRegistry.getFileContent(name, filePath); if (content === undefined) { helpers.error(res, 404, "File not found"); return; } helpers.json(res, { content }); } catch (err: unknown) { helpers.error( res, httpStatusForPiResourceError(err), errorMessage(err, "Failed to load skill file"), ); } } function handleListDirectories(url: URL, res: ServerResponse): void { const root = url.searchParams.get("root"); const dirs = root ? scanDirectories(root) : discoverProjects(); helpers.json(res, { directories: dirs }); } function handleGetHostPathStatus(url: URL, res: ServerResponse): void { const path = url.searchParams.get("path")?.trim(); if (!path) { helpers.json(res, { status: { path: "", resolvedPath: "", exists: false, isDirectory: false, isFile: false, issue: "missing", message: "Path required", }, }); return; } helpers.json(res, { status: getHostPathStatus(path) }); } function handleListHostPathCompletions(url: URL, res: ServerResponse): void { const prefix = url.searchParams.get("prefix") ?? ""; const limit = Number.parseInt(url.searchParams.get("limit") ?? "20", 10) || 20; helpers.json(res, { completions: completeHostPath(prefix, limit) }); } async function handleSetPiResourceEnabled( req: IncomingMessage, res: ServerResponse, ): Promise { const body = await helpers.parseBody<{ workspaceId?: unknown; cwd?: unknown; type?: unknown; path?: unknown; enabled?: unknown; }>(req); const type = body.type; if (type !== "skills" && type !== "extensions") { helpers.error(res, 400, "type must be skills or extensions"); return; } if (typeof body.path !== "string" || body.path.trim().length === 0) { helpers.error(res, 400, "path required"); return; } if (typeof body.enabled !== "boolean") { helpers.error(res, 400, "enabled boolean required"); return; } if (body.workspaceId !== undefined && typeof body.workspaceId !== "string") { helpers.error(res, 400, "workspaceId must be a string"); return; } try { const scope = resolveScopedResourceCwd({ workspaceId: typeof body.workspaceId === "string" ? body.workspaceId : undefined, cwd: typeof body.cwd === "string" ? body.cwd : undefined, }); await setPiResourceEnabled({ cwd: resolveResourceCwd(scope.cwd), type, path: body.path, enabled: body.enabled, preferProjectScope: scope.workspaceScoped || Boolean(scope.cwd?.trim().length), }); helpers.json(res, { ok: true }); } catch (err: unknown) { helpers.error( res, httpStatusForPiResourceError(err), errorMessage(err, "Failed to update Pi resource settings"), ); } } async function handleCreateHostPath(req: IncomingMessage, res: ServerResponse): Promise { const body = await helpers.parseBody<{ path?: unknown; confirmed?: unknown }>(req); if (body.confirmed !== true) { helpers.error(res, 400, "Directory creation requires explicit confirmation"); return; } if (typeof body.path !== "string" || body.path.trim().length === 0) { helpers.error(res, 400, "path required"); return; } try { const result = createHostWorkspaceDirectory(body.path); helpers.json(res, result, result.created ? 201 : 200); } catch (err: unknown) { if (err instanceof HostPathCreateError) { helpers.error(res, err.status, err.message); return; } const message = err instanceof Error ? err.message : "Failed to create directory"; helpers.error(res, 500, message); } } return async ({ method, path, url, req, res }) => { if (path === "/skills" && method === "GET") { await handleListSkills(url, res); return true; } if (path === "/skills/rescan" && method === "POST") { handleRescanSkills(res); return true; } if (path === "/extensions" && method === "GET") { await handleListExtensions(url, res); return true; } if (path === "/pi/resources/enabled" && method === "POST") { await handleSetPiResourceEnabled(req, res); return true; } // Skill detail + file access const skillFileMatch = path.match(/^\/skills\/([^/]+)\/file$/); if (skillFileMatch && method === "GET") { await handleGetSkillFile(skillFileMatch[1], url, res); return true; } const skillDetailMatch = path.match(/^\/skills\/([^/]+)$/); if (skillDetailMatch && method === "GET") { await handleGetSkillDetail(skillDetailMatch[1], url, res); return true; } // Host discovery if (path === "/host/directories" && method === "GET") { handleListDirectories(url, res); return true; } if (path === "/host/path/status" && method === "GET") { handleGetHostPathStatus(url, res); return true; } if (path === "/host/path/completions" && method === "GET") { handleListHostPathCompletions(url, res); return true; } if (path === "/host/path/create" && method === "POST") { await handleCreateHostPath(req, res); return true; } return false; }; }