Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/hook/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ import { urlToMarkdown } from "@plannotator/shared/url-to-markdown";
import { fetchRef, createWorktree, removeWorktree, ensureObjectAvailable } from "@plannotator/shared/worktree";
import { parsePRUrl, checkPRAuth, fetchPR, getCliName, getCliInstallUrl, getMRLabel, getMRNumberLabel, getDisplayRepo } from "@plannotator/server/pr";
import { writeRemoteShareLink } from "@plannotator/server/share-url";
import { resolveMarkdownFile, hasMarkdownFiles } from "@plannotator/shared/resolve-file";
import { resolveMarkdownFile, resolveUserPath, hasMarkdownFiles } from "@plannotator/shared/resolve-file";
import { FILE_BROWSER_EXCLUDED } from "@plannotator/shared/reference-common";
import { statSync, rmSync, realpathSync, existsSync } from "fs";
import { parseRemoteUrl } from "@plannotator/shared/repo";
Expand Down Expand Up @@ -502,7 +502,7 @@ if (args[0] === "sessions") {
sourceInfo = filePath; // Full URL for source attribution
} else {
// Check if the argument is a directory (folder annotation mode)
const resolvedArg = path.resolve(projectRoot, filePath);
const resolvedArg = resolveUserPath(filePath, projectRoot);
let isFolder = false;
try {
isFolder = statSync(resolvedArg).isDirectory();
Expand Down
4 changes: 2 additions & 2 deletions apps/opencode-plugin/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
import { getGitContext, runGitDiffWithContext } from "@plannotator/server/git";
import { parsePRUrl, checkPRAuth, fetchPR, getCliName, getMRLabel, getMRNumberLabel, getDisplayRepo } from "@plannotator/server/pr";
import { loadConfig, resolveDefaultDiffType, resolveUseJina } from "@plannotator/shared/config";
import { resolveMarkdownFile } from "@plannotator/shared/resolve-file";
import { resolveMarkdownFile, resolveUserPath } from "@plannotator/shared/resolve-file";
import { htmlToMarkdown } from "@plannotator/shared/html-to-markdown";
import { urlToMarkdown } from "@plannotator/shared/url-to-markdown";
import { statSync } from "fs";
Expand Down Expand Up @@ -178,7 +178,7 @@ export async function handleAnnotateCommand(
sourceInfo = filePath;
} else {
const projectRoot = process.cwd();
const resolvedArg = path.resolve(projectRoot, filePath);
const resolvedArg = resolveUserPath(filePath, projectRoot);

if (/\.html?$/i.test(resolvedArg)) {
// HTML file annotation — convert to markdown via Turndown
Expand Down
6 changes: 3 additions & 3 deletions apps/pi-extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
*/

import { existsSync, readFileSync, statSync } from "node:fs";
import { resolve, basename } from "node:path";
import { basename, resolve } from "node:path";
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
import { Type } from "@mariozechner/pi-ai";
import type {
Expand All @@ -34,7 +34,7 @@ import {
parseChecklist,
} from "./generated/checklist.js";
import { planDenyFeedback } from "./generated/feedback-templates.js";
import { hasMarkdownFiles } from "./generated/resolve-file.js";
import { hasMarkdownFiles, resolveUserPath } from "./generated/resolve-file.js";
import { FILE_BROWSER_EXCLUDED } from "./generated/reference-common.js";
import { htmlToMarkdown } from "./generated/html-to-markdown.js";
import { urlToMarkdown } from "./generated/url-to-markdown.js";
Expand Down Expand Up @@ -417,7 +417,7 @@ export default function plannotator(pi: ExtensionAPI): void {
absolutePath = filePath;
sourceInfo = filePath;
} else {
absolutePath = resolve(ctx.cwd, filePath);
absolutePath = resolveUserPath(filePath, ctx.cwd);
if (!existsSync(absolutePath)) {
ctx.ui.notify(`File not found: ${absolutePath}`, "error");
return;
Expand Down
6 changes: 6 additions & 0 deletions apps/pi-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,11 @@
},
"peerDependencies": {
"@mariozechner/pi-coding-agent": ">=0.53.0"
},
"devDependencies": {
"@mariozechner/pi-coding-agent": ">=0.53.0",
"@mariozechner/pi-agent-core": ">=0.53.0",
"@mariozechner/pi-ai": ">=0.53.0",
"@mariozechner/pi-tui": ">=0.53.0"
}
}
8 changes: 4 additions & 4 deletions apps/pi-extension/server/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
detectObsidianVaults,
} from "../generated/integrations-common.js";
import { sanitizeTag } from "../generated/project.js";
import { resolveUserPath } from "../generated/resolve-file.js";

export type { ObsidianConfig, BearConfig, OctarineConfig, IntegrationResult };
export {
Expand Down Expand Up @@ -111,11 +112,10 @@ export async function saveToObsidian(
): Promise<IntegrationResult> {
try {
const { vaultPath, folder, plan } = config;
let normalizedVault = vaultPath.trim();
if (normalizedVault.startsWith("~")) {
const home = process.env.HOME || process.env.USERPROFILE || "";
normalizedVault = join(home, normalizedVault.slice(1));
if (!vaultPath?.trim()) {
return { success: false, error: "Vault path is required" };
}
const normalizedVault = resolveUserPath(vaultPath);
if (!existsSync(normalizedVault))
return {
success: false,
Expand Down
22 changes: 14 additions & 8 deletions apps/pi-extension/server/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ import {
FILE_BROWSER_EXCLUDED,
} from "../generated/reference-common.js";
import { detectObsidianVaults } from "../generated/integrations-common.js";
import { resolveMarkdownFile, isWithinProjectRoot } from "../generated/resolve-file.js";
import {
isAbsoluteUserPath,
resolveMarkdownFile,
resolveUserPath,
isWithinProjectRoot,
} from "../generated/resolve-file.js";
import { htmlToMarkdown } from "../generated/html-to-markdown.js";

type Res = ServerResponse;
Expand Down Expand Up @@ -62,12 +67,13 @@ export function handleDocRequest(res: Res, url: URL): void {
// server (see serverAnnotate.ts /api/doc route). The standalone HTML
// block below (no base) retains its cwd-based containment check.
const base = url.searchParams.get("base");
const resolvedBase = base ? resolveUserPath(base) : null;
if (
base &&
!requestedPath.startsWith("/") &&
resolvedBase &&
!isAbsoluteUserPath(requestedPath) &&
/\.(mdx?|html?)$/i.test(requestedPath)
) {
const fromBase = resolvePath(base, requestedPath);
const fromBase = resolveUserPath(requestedPath, resolvedBase);
try {
if (existsSync(fromBase)) {
const raw = readFileSync(fromBase, "utf-8");
Expand All @@ -83,7 +89,7 @@ export function handleDocRequest(res: Res, url: URL): void {
// HTML files: resolve directly (not via resolveMarkdownFile which only handles .md/.mdx)
const projectRoot = process.cwd();
if (/\.html?$/i.test(requestedPath)) {
const resolvedHtml = resolvePath(base || projectRoot, requestedPath);
const resolvedHtml = resolveUserPath(requestedPath, resolvedBase || projectRoot);
if (!isWithinProjectRoot(resolvedHtml, projectRoot)) {
json(res, { error: "Access denied: path is outside project root" }, 403);
return;
Expand Down Expand Up @@ -136,7 +142,7 @@ export function handleObsidianFilesRequest(res: Res, url: URL): void {
json(res, { error: "Missing vaultPath parameter" }, 400);
return;
}
const resolvedVault = resolvePath(vaultPath);
const resolvedVault = resolveUserPath(vaultPath);
if (!existsSync(resolvedVault) || !statSync(resolvedVault).isDirectory()) {
json(res, { error: "Invalid vault path" }, 400);
return;
Expand All @@ -162,7 +168,7 @@ export function handleObsidianDocRequest(res: Res, url: URL): void {
json(res, { error: "Only markdown files are supported" }, 400);
return;
}
const resolvedVault = resolvePath(vaultPath);
const resolvedVault = resolveUserPath(vaultPath);
let resolvedFile = resolvePath(resolvedVault, filePath);

// Bare filename search within vault
Expand Down Expand Up @@ -214,7 +220,7 @@ export function handleFileBrowserRequest(res: Res, url: URL): void {
json(res, { error: "Missing dirPath parameter" }, 400);
return;
}
const resolvedDir = resolvePath(dirPath);
const resolvedDir = resolveUserPath(dirPath);
if (!existsSync(resolvedDir) || !statSync(resolvedDir).isDirectory()) {
json(res, { error: "Invalid directory path" }, 400);
return;
Expand Down
20 changes: 12 additions & 8 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"build:vscode": "bun run --cwd apps/vscode-extension build",
"package:vscode": "bun run --cwd apps/vscode-extension package",
"test": "bun test",
"typecheck": "tsc --noEmit -p packages/shared/tsconfig.json && tsc --noEmit -p packages/ai/tsconfig.json && tsc --noEmit -p packages/server/tsconfig.json"
"typecheck": "bash apps/pi-extension/vendor.sh && tsc --noEmit -p packages/shared/tsconfig.json && tsc --noEmit -p packages/ai/tsconfig.json && tsc --noEmit -p packages/server/tsconfig.json && tsc --noEmit -p apps/pi-extension/tsconfig.json"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.92",
Expand Down
12 changes: 5 additions & 7 deletions packages/server/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
buildBearContent,
detectObsidianVaults,
} from "@plannotator/shared/integrations-common";
import { resolveUserPath } from "@plannotator/shared/resolve-file";

export type { ObsidianConfig, BearConfig, OctarineConfig, IntegrationResult };
export { detectObsidianVaults, extractTitle, generateFrontmatter, generateFilename, generateOctarineFrontmatter, stripH1, buildHashtags, buildBearContent };
Expand Down Expand Up @@ -98,15 +99,12 @@ export async function saveToObsidian(
try {
const { vaultPath, folder, plan } = config;

// Normalize path (handle ~ on Unix, forward/back slashes)
let normalizedVault = vaultPath.trim();

// Expand ~ to home directory (Unix/macOS)
if (normalizedVault.startsWith("~")) {
const home = process.env.HOME || process.env.USERPROFILE || "";
normalizedVault = join(home, normalizedVault.slice(1));
if (!vaultPath?.trim()) {
return { success: false, error: "Vault path is required" };
}

const normalizedVault = resolveUserPath(vaultPath);

// Validate vault path exists and is a directory
if (!existsSync(normalizedVault)) {
return {
Expand Down
22 changes: 14 additions & 8 deletions packages/server/reference-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import { existsSync, statSync } from "fs";
import { resolve } from "path";
import { buildFileTree, FILE_BROWSER_EXCLUDED } from "@plannotator/shared/reference-common";
import { detectObsidianVaults } from "./integrations";
import { resolveMarkdownFile, isWithinProjectRoot } from "@plannotator/shared/resolve-file";
import {
isAbsoluteUserPath,
resolveMarkdownFile,
resolveUserPath,
isWithinProjectRoot,
} from "@plannotator/shared/resolve-file";
import { htmlToMarkdown } from "@plannotator/shared/html-to-markdown";

// --- Route handlers ---
Expand All @@ -29,12 +34,13 @@ export async function handleDoc(req: Request): Promise<Response> {
// server (see annotate.ts /api/doc route). The standalone HTML block
// below (no base) retains its cwd-based containment check.
const base = url.searchParams.get("base");
const resolvedBase = base ? resolveUserPath(base) : null;
if (
base &&
!requestedPath.startsWith("/") &&
resolvedBase &&
!isAbsoluteUserPath(requestedPath) &&
/\.(mdx?|html?)$/i.test(requestedPath)
) {
const fromBase = resolve(base, requestedPath);
const fromBase = resolveUserPath(requestedPath, resolvedBase);
try {
const file = Bun.file(fromBase);
if (await file.exists()) {
Expand All @@ -50,7 +56,7 @@ export async function handleDoc(req: Request): Promise<Response> {
// HTML files: resolve directly (not via resolveMarkdownFile which only handles .md/.mdx)
const projectRoot = process.cwd();
if (/\.html?$/i.test(requestedPath)) {
const resolvedHtml = resolve(base || projectRoot, requestedPath);
const resolvedHtml = resolveUserPath(requestedPath, resolvedBase || projectRoot);
if (!isWithinProjectRoot(resolvedHtml, projectRoot)) {
return Response.json({ error: "Access denied: path is outside project root" }, { status: 403 });
}
Expand Down Expand Up @@ -109,7 +115,7 @@ export async function handleObsidianFiles(req: Request): Promise<Response> {
);
}

const resolvedVault = resolve(vaultPath);
const resolvedVault = resolveUserPath(vaultPath);
if (!existsSync(resolvedVault) || !statSync(resolvedVault).isDirectory()) {
return Response.json({ error: "Invalid vault path" }, { status: 400 });
}
Expand Down Expand Up @@ -154,7 +160,7 @@ export async function handleObsidianDoc(req: Request): Promise<Response> {
);
}

const resolvedVault = resolve(vaultPath);
const resolvedVault = resolveUserPath(vaultPath);
let resolvedFile = resolve(resolvedVault, filePath);

// If direct path doesn't exist and it's a bare filename, search the vault
Expand Down Expand Up @@ -220,7 +226,7 @@ export async function handleFileBrowserFiles(req: Request): Promise<Response> {
);
}

const resolvedDir = resolve(dirPath);
const resolvedDir = resolveUserPath(dirPath);
if (!existsSync(resolvedDir) || !statSync(resolvedDir).isDirectory()) {
return Response.json({ error: "Invalid directory path" }, { status: 400 });
}
Expand Down
Loading