Files
backnotprop__plannotator/packages/shared/resolve-file.ts
T

301 lines
8.5 KiB
TypeScript
Raw Normal View History

feat(pi): complete Pi server rewrite — modular architecture, full Bun parity, shared code extraction (#382) * feat(pi): add missing endpoints to plan, review, and annotate servers Phase 1-3 of Pi endpoint parity: Plan server: image, upload, draft, editor-annotations, agents, favicon, linked documents, Obsidian vaults/files/doc, file browser, VS Code diff Annotate server: image, upload, draft, favicon, linked documents, file browser Review server: extract shared handlers, add favicon Shared utilities extracted from review server inline code into reusable functions (handleImageRequest, handleUploadRequest, handleDraftRequest, handleFavicon). Reference handlers (doc, Obsidian, file browser) implemented using Node.js fs APIs replacing Bun.Glob/Bun.file. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(pi): add PR review endpoints and Node.js PR runtime adapter Phase 4 of Pi endpoint parity: - Node.js PRRuntime using child_process.spawn (matches Bun adapter pattern) - GET /api/pr-context — fetch PR summary, comments, checks - POST /api/pr-action — submit review to GitHub/GitLab - PR mode guards on /api/diff/switch and /api/git-add - /api/diff response includes prMetadata and platformUser in PR mode - /api/file-content fetches from platform API in PR mode - Build script copies pr-provider, pr-github, pr-gitlab from shared Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(pi): wire AI backbone with Node.js Pi SDK provider Phase 5 of Pi endpoint parity: - Create packages/ai/providers/pi-sdk-node.ts — PiProcessNode class using child_process.spawn instead of Bun.spawn, same RPC protocol - Register 4 AI providers in Pi review server (claude-agent-sdk, codex-sdk, pi-sdk-node, opencode-sdk) with graceful degradation - Route /api/ai/* endpoints through createAIEndpoints handlers - Pipe Web Response → node:http response with ReadableStream support for SSE streaming - Dispose AI sessions and registry on server stop Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(pi): address parity audit findings across all three servers Plan server: - /api/plan: add repoInfo and projectRoot to response - /api/approve: pass agentSwitch and permissionMode in decision - Update decision promise type to include agentSwitch, permissionMode Review server: - /api/diff/switch: pass gitContext.cwd to runGitDiff - /api/file-content: pass gitContext.cwd to getFileContentsForDiffCore - /api/git-add: add fallback to gitContext.cwd when worktree parse fails Annotate server: - /api/plan: add repoInfo and projectRoot to response - /api/feedback: capture annotations array (was silently dropped) - Update decision promise type to include annotations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(pi): complete parity — integrations, planSave, save-notes Ports all remaining missing functionality: - Node.js versions of saveToObsidian, saveToBear, saveToOctarine (Bun.write → writeFileSync, Bun.$ → spawn) - Node.js detectProjectNameSync (Bun.$ → execSync) - extractTags, generateFrontmatter, generateFilename, extractTitle - POST /api/save-notes — decoupled note saving - POST /api/approve — full implementation: note integrations, planSave snapshots, saveAnnotations, saveFinalSnapshot - POST /api/deny — planSave snapshots on denial - Import saveAnnotations, saveFinalSnapshot from storage.js Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(pi): wire domain module imports and fix type errors - Add all missing imports from ./server/* domain modules to server.ts - Export interfaces from integrations.ts (ObsidianConfig, BearConfig, etc.) - Move toWebRequest to helpers.ts, remove duplicate from handlers.ts - Add git() helper to project.ts (was in server.ts, needed by getRepoInfo) - Fix os default import → named imports in handlers.ts and network.ts - Fix readdirSync Dirent type in reference.ts - Fix Headers.entries() → forEach for Node compat in AI endpoint piping - Fix ReadableStream type cast in AI SSE streaming - Fix matchAll iterator compat in integrations.ts (use while + exec) - Cast pi-sdk provider config to any (PiSDKConfig not in base union) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(pi): move generated shared files to generated/ directory Moves all build-time copied shared files (feedback-templates, review-core, storage, draft, project, pr-provider, pr-github, pr-gitlab) from the pi-extension root into generated/ subdirectory. Updates build script to output there. Updates all imports in server.ts, index.ts, and server/ domain modules to use ./generated/ paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(pi): replace hand-maintained utils.ts with generated checklist utils.ts was a manual copy of parseChecklist, extractDoneSteps, and markCompletedSteps from packages/shared/checklist.ts. Add checklist to the build-time copy list and import from generated/checklist.js. Delete the redundant utils.ts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(pi): gitignore generated/ and built HTML files These are build artifacts created by `bun run build:pi`. Untrack them and add .gitignore to prevent re-adding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(pi): split server.ts into domain-organized modules - server.ts is now a barrel re-exporting from server/ modules - server/serverPlan.ts — plan review server - server/serverReview.ts — code review server - server/serverAnnotate.ts — annotate server - server/helpers.ts — add requestUrl() to eliminate non-null assertions - server/project.ts — linter fix (sanitizeTag import path) - packages/ai/package.json — add pi-sdk-node export entry - index.ts — fix waitForDone non-null assertion with guard check, update imports for generated/checklist.js Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(pi): parity audit fixes + shared code extraction Systematic side-by-side audit of Pi vs Bun servers (A1-A22, B1-B2 complete). Fixes found during audit: - PlanServerResult.waitForDecision missing savedPath/agentSwitch/permissionMode - Missing permissionMode option and /api/plan response field - editorAnnotations created unnecessarily in archive mode - repoInfo called per-request instead of cached at init - Approve handler missing effectivePermissionMode fallback - Deny handler missing savedPath in decision resolution - Archive /api/plan response had extra pasteApiUrl - Missing GET method guards on archive/plans, archive/plan, doc, obsidian/files, obsidian/doc, reference/files - Review server had stray pasteApiUrl option/response field - AI getCwd missing worktree support Shared code extraction: - packages/shared/favicon.ts — single source for favicon SVG - packages/shared/integrations-common.ts — note app pure functions - packages/shared/reference-common.ts — file tree building - packages/shared/repo.ts — git remote parsing - Updated all consumers to import from shared sources For provenance purposes, this commit was AI assisted. * fix: parity audit B3-C10 — review + annotate server fixes Review server (B3-B17): - diff/switch missing try/catch error handling - git-add parseBody outside try/catch - feedback missing try/catch error handling - Unknown /api/ai/* paths now return 404 (both Bun and Pi) Annotate server (C1-C10): - Bun annotate server missing pasteApiUrl (short URL sharing broken) - Added pasteApiUrl to Bun options, response, and both hook callers - Pi repoInfo called per-request instead of cached at init - Pi feedback missing try/catch error handling - Missing GET method guards on doc and reference/files For provenance purposes, this commit was AI assisted. * fix: parity audit D3-D5 — draft error handling, editor annotations, resolve-file extraction D3: Pi draft save handler missing error handling — added .catch() with 500 + console.error D4: Pi editor annotation POST missing try/catch — added with "Invalid JSON" 400 D5: Extracted resolveMarkdownFile to packages/shared/resolve-file.ts - Replaced Bun.Glob with runtime-agnostic walkMarkdownFiles (readdirSync) - Made function sync (no longer async) - Pi handleDocRequest now uses shared resolveMarkdownFile instead of inline resolution - Gains Windows path normalization, isWithinProjectRoot security check - Deleted packages/server/resolve-file.ts re-export, consumers import from shared - Cleaned up stale await calls in hook entry, reference handler, and tests - All 19 resolve-file tests pass For provenance purposes, this commit was AI assisted. * fix: parity audit D6-D10 — integrations, PR naming, shared modules D6: Fixed broken detectProjectNameSync — was using require() for non-existent exports. Now uses basename + sanitizeTag directly. D7: Renamed checkAuth → checkPRAuth, getUser → getPRUser across Bun server, hook, and OpenCode plugin to match Pi naming. Also fixed stale resolve-file import in OpenCode plugin. D8-D10: Verified clean — ide, project detection, network. For provenance purposes, this commit was AI assisted. * update openpackage.yml * fix: bump Pi git-add test timeout to 15s for parallel suite stability For provenance purposes, this commit was AI assisted. * test: add route parity test — Bun ↔ Pi server route drift detection For provenance purposes, this commit was AI assisted. * fix(ci): update Pi generate step to use generated/ directory with full file list The Pi extension was refactored to use generated/ subdirectory but the CI generate step still used the old flat layout with a subset of files. For provenance purposes, this commit was AI assisted. * fix(ci): update release workflow Pi generate step to match new layout Same stale generate step as test.yml — old flat layout, missing files. For provenance purposes, this commit was AI assisted. * fix(pi): update files array for modular server layout The files array still referenced the old flat layout (server.ts monolith, root-level generated files, deleted utils.ts). npm publish would have produced a broken package missing server/ and generated/ directories. For provenance purposes, this commit was AI assisted. * feat: add TypeScript type-checking to CI pipeline - Fix broken barrel export: buildFileTree/VaultNode re-exported from @plannotator/shared instead of reference-handlers (P1 bug) - Fix server.port type narrowing in all 3 servers - Fix AI provider type errors (claude-agent-sdk, codex-sdk, opencode-sdk, pi-sdk) - Extract mapPiEvent to pi-events.ts to break Bun→Node type chain - Add tsconfig.json to packages/shared, packages/ai, packages/server, apps/pi-extension - Add `typecheck` script to root package.json - Add type-check step to test.yml and release.yml CI workflows For provenance purposes, this commit was AI assisted. * fix(ci): use bun-types instead of @types/node for typecheck CI environment has bun-types (includes Node types) but not @types/node as a standalone package. For provenance purposes, this commit was AI assisted. * fix(ci): add @types/node for Node-runtime type checks Pi extension and packages/shared run on Node, not Bun — they should type-check against @types/node, not bun-types. Added @types/node as a dev dependency so CI resolves it. For provenance purposes, this commit was AI assisted. * fix: cast Uint8Array.buffer to ArrayBuffer for TS 5.9 compat crypto.subtle.importKey expects BufferSource, but TS 5.9 is stricter about Uint8Array.buffer being ArrayBufferLike (includes SharedArrayBuffer) vs ArrayBuffer. Explicit cast resolves the overload mismatch. Astro pulls in TS 5.9 transitively, so CI resolves a different TypeScript version than local dev. This fix works on both 5.8 and 5.9. For provenance purposes, this commit was AI assisted. * fix(ci): add bun-types as explicit devDependency CI's bun install doesn't hoist bun-types to root node_modules when it's only a transitive dep of @types/bun. Adding it as a direct devDependency guarantees tsc can resolve it. For provenance purposes, this commit was AI assisted. * fix(ci): remove Pi extension from typecheck Pi extension depends on @mariozechner/pi-* peer dependencies that aren't installed in CI. Type-checking it requires Pi's runtime environment. The three packages we check (shared, ai, server) are sufficient to catch barrel export bugs and type errors. Pi extension coverage comes from route parity tests and bun test. For provenance purposes, this commit was AI assisted. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 00:44:54 -07:00
/**
* Smart markdown file resolution.
*
* Resolves a user-provided path to an absolute file path using three strategies:
* 1. Exact path (absolute or relative to cwd)
* 2. Case-insensitive relative path search within project root
* 3. Case-insensitive bare filename search within project root
*
* Used by both the CLI (`plannotator annotate`) and the `/api/doc` endpoint.
*/
import { isAbsolute, join, resolve, win32 } from "path";
import { existsSync, readdirSync, type Dirent } from "fs";
const MARKDOWN_PATH_REGEX = /\.mdx?$/i;
const WINDOWS_DRIVE_PATH_PATTERNS = [
/^\/cygdrive\/([a-zA-Z])\/(.+)$/,
/^\/([a-zA-Z])\/(.+)$/,
];
const IGNORED_DIRS = [
"node_modules/",
".git/",
"dist/",
"build/",
".next/",
"__pycache__/",
".obsidian/",
".trash/",
];
export type ResolveResult =
| { kind: "found"; path: string }
| { kind: "not_found"; input: string }
| { kind: "ambiguous"; input: string; matches: string[] };
function normalizeSeparators(input: string): string {
return input.replace(/\\/g, "/");
}
function stripTrailingSlashes(input: string): string {
return input.replace(/\/+$/, "");
}
function normalizeComparablePath(input: string): string {
return stripTrailingSlashes(normalizeSeparators(resolve(input)));
}
function isWithinProjectRoot(candidate: string, projectRoot: string): boolean {
const normalizedCandidate = normalizeComparablePath(candidate);
const normalizedProjectRoot = normalizeComparablePath(projectRoot);
return (
normalizedCandidate === normalizedProjectRoot ||
normalizedCandidate.startsWith(`${normalizedProjectRoot}/`)
);
}
function getLowercaseBasename(input: string): string {
const normalizedInput = normalizeSeparators(input);
return normalizedInput.split("/").pop()!.toLowerCase();
}
function getLookupKey(input: string, isBareFilename: boolean): string {
return isBareFilename ? getLowercaseBasename(input) : input.toLowerCase();
}
function resolveAbsolutePath(
input: string,
platform = process.platform,
): string {
// Use win32.resolve for Windows paths regardless of reported platform
return platform === "win32" || hasWindowsDriveLetter(input)
? win32.resolve(input)
: resolve(input);
}
function isSearchableMarkdownPath(input: string): boolean {
return MARKDOWN_PATH_REGEX.test(input.trim());
}
function stripWrappingQuotes(input: string): string {
if (input.length < 2) {
return input;
}
const first = input[0];
const last = input[input.length - 1];
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
return input.slice(1, -1);
}
return input;
}
feat(pi): complete Pi server rewrite — modular architecture, full Bun parity, shared code extraction (#382) * feat(pi): add missing endpoints to plan, review, and annotate servers Phase 1-3 of Pi endpoint parity: Plan server: image, upload, draft, editor-annotations, agents, favicon, linked documents, Obsidian vaults/files/doc, file browser, VS Code diff Annotate server: image, upload, draft, favicon, linked documents, file browser Review server: extract shared handlers, add favicon Shared utilities extracted from review server inline code into reusable functions (handleImageRequest, handleUploadRequest, handleDraftRequest, handleFavicon). Reference handlers (doc, Obsidian, file browser) implemented using Node.js fs APIs replacing Bun.Glob/Bun.file. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(pi): add PR review endpoints and Node.js PR runtime adapter Phase 4 of Pi endpoint parity: - Node.js PRRuntime using child_process.spawn (matches Bun adapter pattern) - GET /api/pr-context — fetch PR summary, comments, checks - POST /api/pr-action — submit review to GitHub/GitLab - PR mode guards on /api/diff/switch and /api/git-add - /api/diff response includes prMetadata and platformUser in PR mode - /api/file-content fetches from platform API in PR mode - Build script copies pr-provider, pr-github, pr-gitlab from shared Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(pi): wire AI backbone with Node.js Pi SDK provider Phase 5 of Pi endpoint parity: - Create packages/ai/providers/pi-sdk-node.ts — PiProcessNode class using child_process.spawn instead of Bun.spawn, same RPC protocol - Register 4 AI providers in Pi review server (claude-agent-sdk, codex-sdk, pi-sdk-node, opencode-sdk) with graceful degradation - Route /api/ai/* endpoints through createAIEndpoints handlers - Pipe Web Response → node:http response with ReadableStream support for SSE streaming - Dispose AI sessions and registry on server stop Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(pi): address parity audit findings across all three servers Plan server: - /api/plan: add repoInfo and projectRoot to response - /api/approve: pass agentSwitch and permissionMode in decision - Update decision promise type to include agentSwitch, permissionMode Review server: - /api/diff/switch: pass gitContext.cwd to runGitDiff - /api/file-content: pass gitContext.cwd to getFileContentsForDiffCore - /api/git-add: add fallback to gitContext.cwd when worktree parse fails Annotate server: - /api/plan: add repoInfo and projectRoot to response - /api/feedback: capture annotations array (was silently dropped) - Update decision promise type to include annotations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(pi): complete parity — integrations, planSave, save-notes Ports all remaining missing functionality: - Node.js versions of saveToObsidian, saveToBear, saveToOctarine (Bun.write → writeFileSync, Bun.$ → spawn) - Node.js detectProjectNameSync (Bun.$ → execSync) - extractTags, generateFrontmatter, generateFilename, extractTitle - POST /api/save-notes — decoupled note saving - POST /api/approve — full implementation: note integrations, planSave snapshots, saveAnnotations, saveFinalSnapshot - POST /api/deny — planSave snapshots on denial - Import saveAnnotations, saveFinalSnapshot from storage.js Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(pi): wire domain module imports and fix type errors - Add all missing imports from ./server/* domain modules to server.ts - Export interfaces from integrations.ts (ObsidianConfig, BearConfig, etc.) - Move toWebRequest to helpers.ts, remove duplicate from handlers.ts - Add git() helper to project.ts (was in server.ts, needed by getRepoInfo) - Fix os default import → named imports in handlers.ts and network.ts - Fix readdirSync Dirent type in reference.ts - Fix Headers.entries() → forEach for Node compat in AI endpoint piping - Fix ReadableStream type cast in AI SSE streaming - Fix matchAll iterator compat in integrations.ts (use while + exec) - Cast pi-sdk provider config to any (PiSDKConfig not in base union) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(pi): move generated shared files to generated/ directory Moves all build-time copied shared files (feedback-templates, review-core, storage, draft, project, pr-provider, pr-github, pr-gitlab) from the pi-extension root into generated/ subdirectory. Updates build script to output there. Updates all imports in server.ts, index.ts, and server/ domain modules to use ./generated/ paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(pi): replace hand-maintained utils.ts with generated checklist utils.ts was a manual copy of parseChecklist, extractDoneSteps, and markCompletedSteps from packages/shared/checklist.ts. Add checklist to the build-time copy list and import from generated/checklist.js. Delete the redundant utils.ts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(pi): gitignore generated/ and built HTML files These are build artifacts created by `bun run build:pi`. Untrack them and add .gitignore to prevent re-adding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(pi): split server.ts into domain-organized modules - server.ts is now a barrel re-exporting from server/ modules - server/serverPlan.ts — plan review server - server/serverReview.ts — code review server - server/serverAnnotate.ts — annotate server - server/helpers.ts — add requestUrl() to eliminate non-null assertions - server/project.ts — linter fix (sanitizeTag import path) - packages/ai/package.json — add pi-sdk-node export entry - index.ts — fix waitForDone non-null assertion with guard check, update imports for generated/checklist.js Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(pi): parity audit fixes + shared code extraction Systematic side-by-side audit of Pi vs Bun servers (A1-A22, B1-B2 complete). Fixes found during audit: - PlanServerResult.waitForDecision missing savedPath/agentSwitch/permissionMode - Missing permissionMode option and /api/plan response field - editorAnnotations created unnecessarily in archive mode - repoInfo called per-request instead of cached at init - Approve handler missing effectivePermissionMode fallback - Deny handler missing savedPath in decision resolution - Archive /api/plan response had extra pasteApiUrl - Missing GET method guards on archive/plans, archive/plan, doc, obsidian/files, obsidian/doc, reference/files - Review server had stray pasteApiUrl option/response field - AI getCwd missing worktree support Shared code extraction: - packages/shared/favicon.ts — single source for favicon SVG - packages/shared/integrations-common.ts — note app pure functions - packages/shared/reference-common.ts — file tree building - packages/shared/repo.ts — git remote parsing - Updated all consumers to import from shared sources For provenance purposes, this commit was AI assisted. * fix: parity audit B3-C10 — review + annotate server fixes Review server (B3-B17): - diff/switch missing try/catch error handling - git-add parseBody outside try/catch - feedback missing try/catch error handling - Unknown /api/ai/* paths now return 404 (both Bun and Pi) Annotate server (C1-C10): - Bun annotate server missing pasteApiUrl (short URL sharing broken) - Added pasteApiUrl to Bun options, response, and both hook callers - Pi repoInfo called per-request instead of cached at init - Pi feedback missing try/catch error handling - Missing GET method guards on doc and reference/files For provenance purposes, this commit was AI assisted. * fix: parity audit D3-D5 — draft error handling, editor annotations, resolve-file extraction D3: Pi draft save handler missing error handling — added .catch() with 500 + console.error D4: Pi editor annotation POST missing try/catch — added with "Invalid JSON" 400 D5: Extracted resolveMarkdownFile to packages/shared/resolve-file.ts - Replaced Bun.Glob with runtime-agnostic walkMarkdownFiles (readdirSync) - Made function sync (no longer async) - Pi handleDocRequest now uses shared resolveMarkdownFile instead of inline resolution - Gains Windows path normalization, isWithinProjectRoot security check - Deleted packages/server/resolve-file.ts re-export, consumers import from shared - Cleaned up stale await calls in hook entry, reference handler, and tests - All 19 resolve-file tests pass For provenance purposes, this commit was AI assisted. * fix: parity audit D6-D10 — integrations, PR naming, shared modules D6: Fixed broken detectProjectNameSync — was using require() for non-existent exports. Now uses basename + sanitizeTag directly. D7: Renamed checkAuth → checkPRAuth, getUser → getPRUser across Bun server, hook, and OpenCode plugin to match Pi naming. Also fixed stale resolve-file import in OpenCode plugin. D8-D10: Verified clean — ide, project detection, network. For provenance purposes, this commit was AI assisted. * update openpackage.yml * fix: bump Pi git-add test timeout to 15s for parallel suite stability For provenance purposes, this commit was AI assisted. * test: add route parity test — Bun ↔ Pi server route drift detection For provenance purposes, this commit was AI assisted. * fix(ci): update Pi generate step to use generated/ directory with full file list The Pi extension was refactored to use generated/ subdirectory but the CI generate step still used the old flat layout with a subset of files. For provenance purposes, this commit was AI assisted. * fix(ci): update release workflow Pi generate step to match new layout Same stale generate step as test.yml — old flat layout, missing files. For provenance purposes, this commit was AI assisted. * fix(pi): update files array for modular server layout The files array still referenced the old flat layout (server.ts monolith, root-level generated files, deleted utils.ts). npm publish would have produced a broken package missing server/ and generated/ directories. For provenance purposes, this commit was AI assisted. * feat: add TypeScript type-checking to CI pipeline - Fix broken barrel export: buildFileTree/VaultNode re-exported from @plannotator/shared instead of reference-handlers (P1 bug) - Fix server.port type narrowing in all 3 servers - Fix AI provider type errors (claude-agent-sdk, codex-sdk, opencode-sdk, pi-sdk) - Extract mapPiEvent to pi-events.ts to break Bun→Node type chain - Add tsconfig.json to packages/shared, packages/ai, packages/server, apps/pi-extension - Add `typecheck` script to root package.json - Add type-check step to test.yml and release.yml CI workflows For provenance purposes, this commit was AI assisted. * fix(ci): use bun-types instead of @types/node for typecheck CI environment has bun-types (includes Node types) but not @types/node as a standalone package. For provenance purposes, this commit was AI assisted. * fix(ci): add @types/node for Node-runtime type checks Pi extension and packages/shared run on Node, not Bun — they should type-check against @types/node, not bun-types. Added @types/node as a dev dependency so CI resolves it. For provenance purposes, this commit was AI assisted. * fix: cast Uint8Array.buffer to ArrayBuffer for TS 5.9 compat crypto.subtle.importKey expects BufferSource, but TS 5.9 is stricter about Uint8Array.buffer being ArrayBufferLike (includes SharedArrayBuffer) vs ArrayBuffer. Explicit cast resolves the overload mismatch. Astro pulls in TS 5.9 transitively, so CI resolves a different TypeScript version than local dev. This fix works on both 5.8 and 5.9. For provenance purposes, this commit was AI assisted. * fix(ci): add bun-types as explicit devDependency CI's bun install doesn't hoist bun-types to root node_modules when it's only a transitive dep of @types/bun. Adding it as a direct devDependency guarantees tsc can resolve it. For provenance purposes, this commit was AI assisted. * fix(ci): remove Pi extension from typecheck Pi extension depends on @mariozechner/pi-* peer dependencies that aren't installed in CI. Type-checking it requires Pi's runtime environment. The three packages we check (shared, ai, server) are sufficient to catch barrel export bugs and type errors. Pi extension coverage comes from route parity tests and bun test. For provenance purposes, this commit was AI assisted. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 00:44:54 -07:00
/** Check if a path looks like a Windows absolute path (e.g. C:\ or C:/) */
function hasWindowsDriveLetter(input: string): boolean {
return /^[a-zA-Z]:[/\\]/.test(input);
}
/** Cross-platform file existence check using Node fs (more reliable than Bun.file in compiled exes) */
function fileExists(filePath: string): boolean {
try {
return existsSync(filePath);
} catch {
return false;
}
}
/** Recursively walk a directory collecting markdown files, skipping ignored dirs. */
function walkMarkdownFiles(dir: string, root: string, results: string[], ignoredDirs: string[]): void {
let entries: Dirent[];
try {
entries = readdirSync(dir, { withFileTypes: true }) as Dirent[];
} catch {
return;
}
for (const entry of entries) {
if (entry.isDirectory()) {
if (ignoredDirs.some((d) => d === entry.name + "/")) continue;
walkMarkdownFiles(join(dir, entry.name), root, results, ignoredDirs);
} else if (entry.isFile() && /\.mdx?$/i.test(entry.name)) {
const relative = join(dir, entry.name)
.slice(root.length + 1)
.replace(/\\/g, "/");
results.push(relative);
}
}
}
export function normalizeMarkdownPathInput(
input: string,
platform = process.platform,
): string {
if (platform !== "win32") {
return input;
}
for (const pattern of WINDOWS_DRIVE_PATH_PATTERNS) {
const match = input.match(pattern);
if (!match) {
continue;
}
const [, driveLetter, rest] = match;
return `${driveLetter.toUpperCase()}:/${rest}`;
}
return input;
}
export function isAbsoluteMarkdownPath(
input: string,
platform = process.platform,
): boolean {
const normalizedInput = normalizeMarkdownPathInput(input, platform);
// Always check for Windows drive letters (handles compiled Bun exes where
// process.platform may not reflect the actual OS correctly)
if (hasWindowsDriveLetter(normalizedInput)) {
return true;
}
return platform === "win32"
? win32.isAbsolute(normalizedInput)
: isAbsolute(normalizedInput);
}
/**
* Resolve a markdown file path within a project root.
*
* @param input - User-provided path (absolute, relative, or bare filename)
* @param projectRoot - Project root directory to search within
*/
function resolveMarkdownFileCore(
feat(pi): complete Pi server rewrite — modular architecture, full Bun parity, shared code extraction (#382) * feat(pi): add missing endpoints to plan, review, and annotate servers Phase 1-3 of Pi endpoint parity: Plan server: image, upload, draft, editor-annotations, agents, favicon, linked documents, Obsidian vaults/files/doc, file browser, VS Code diff Annotate server: image, upload, draft, favicon, linked documents, file browser Review server: extract shared handlers, add favicon Shared utilities extracted from review server inline code into reusable functions (handleImageRequest, handleUploadRequest, handleDraftRequest, handleFavicon). Reference handlers (doc, Obsidian, file browser) implemented using Node.js fs APIs replacing Bun.Glob/Bun.file. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(pi): add PR review endpoints and Node.js PR runtime adapter Phase 4 of Pi endpoint parity: - Node.js PRRuntime using child_process.spawn (matches Bun adapter pattern) - GET /api/pr-context — fetch PR summary, comments, checks - POST /api/pr-action — submit review to GitHub/GitLab - PR mode guards on /api/diff/switch and /api/git-add - /api/diff response includes prMetadata and platformUser in PR mode - /api/file-content fetches from platform API in PR mode - Build script copies pr-provider, pr-github, pr-gitlab from shared Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(pi): wire AI backbone with Node.js Pi SDK provider Phase 5 of Pi endpoint parity: - Create packages/ai/providers/pi-sdk-node.ts — PiProcessNode class using child_process.spawn instead of Bun.spawn, same RPC protocol - Register 4 AI providers in Pi review server (claude-agent-sdk, codex-sdk, pi-sdk-node, opencode-sdk) with graceful degradation - Route /api/ai/* endpoints through createAIEndpoints handlers - Pipe Web Response → node:http response with ReadableStream support for SSE streaming - Dispose AI sessions and registry on server stop Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(pi): address parity audit findings across all three servers Plan server: - /api/plan: add repoInfo and projectRoot to response - /api/approve: pass agentSwitch and permissionMode in decision - Update decision promise type to include agentSwitch, permissionMode Review server: - /api/diff/switch: pass gitContext.cwd to runGitDiff - /api/file-content: pass gitContext.cwd to getFileContentsForDiffCore - /api/git-add: add fallback to gitContext.cwd when worktree parse fails Annotate server: - /api/plan: add repoInfo and projectRoot to response - /api/feedback: capture annotations array (was silently dropped) - Update decision promise type to include annotations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(pi): complete parity — integrations, planSave, save-notes Ports all remaining missing functionality: - Node.js versions of saveToObsidian, saveToBear, saveToOctarine (Bun.write → writeFileSync, Bun.$ → spawn) - Node.js detectProjectNameSync (Bun.$ → execSync) - extractTags, generateFrontmatter, generateFilename, extractTitle - POST /api/save-notes — decoupled note saving - POST /api/approve — full implementation: note integrations, planSave snapshots, saveAnnotations, saveFinalSnapshot - POST /api/deny — planSave snapshots on denial - Import saveAnnotations, saveFinalSnapshot from storage.js Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(pi): wire domain module imports and fix type errors - Add all missing imports from ./server/* domain modules to server.ts - Export interfaces from integrations.ts (ObsidianConfig, BearConfig, etc.) - Move toWebRequest to helpers.ts, remove duplicate from handlers.ts - Add git() helper to project.ts (was in server.ts, needed by getRepoInfo) - Fix os default import → named imports in handlers.ts and network.ts - Fix readdirSync Dirent type in reference.ts - Fix Headers.entries() → forEach for Node compat in AI endpoint piping - Fix ReadableStream type cast in AI SSE streaming - Fix matchAll iterator compat in integrations.ts (use while + exec) - Cast pi-sdk provider config to any (PiSDKConfig not in base union) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(pi): move generated shared files to generated/ directory Moves all build-time copied shared files (feedback-templates, review-core, storage, draft, project, pr-provider, pr-github, pr-gitlab) from the pi-extension root into generated/ subdirectory. Updates build script to output there. Updates all imports in server.ts, index.ts, and server/ domain modules to use ./generated/ paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(pi): replace hand-maintained utils.ts with generated checklist utils.ts was a manual copy of parseChecklist, extractDoneSteps, and markCompletedSteps from packages/shared/checklist.ts. Add checklist to the build-time copy list and import from generated/checklist.js. Delete the redundant utils.ts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(pi): gitignore generated/ and built HTML files These are build artifacts created by `bun run build:pi`. Untrack them and add .gitignore to prevent re-adding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(pi): split server.ts into domain-organized modules - server.ts is now a barrel re-exporting from server/ modules - server/serverPlan.ts — plan review server - server/serverReview.ts — code review server - server/serverAnnotate.ts — annotate server - server/helpers.ts — add requestUrl() to eliminate non-null assertions - server/project.ts — linter fix (sanitizeTag import path) - packages/ai/package.json — add pi-sdk-node export entry - index.ts — fix waitForDone non-null assertion with guard check, update imports for generated/checklist.js Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(pi): parity audit fixes + shared code extraction Systematic side-by-side audit of Pi vs Bun servers (A1-A22, B1-B2 complete). Fixes found during audit: - PlanServerResult.waitForDecision missing savedPath/agentSwitch/permissionMode - Missing permissionMode option and /api/plan response field - editorAnnotations created unnecessarily in archive mode - repoInfo called per-request instead of cached at init - Approve handler missing effectivePermissionMode fallback - Deny handler missing savedPath in decision resolution - Archive /api/plan response had extra pasteApiUrl - Missing GET method guards on archive/plans, archive/plan, doc, obsidian/files, obsidian/doc, reference/files - Review server had stray pasteApiUrl option/response field - AI getCwd missing worktree support Shared code extraction: - packages/shared/favicon.ts — single source for favicon SVG - packages/shared/integrations-common.ts — note app pure functions - packages/shared/reference-common.ts — file tree building - packages/shared/repo.ts — git remote parsing - Updated all consumers to import from shared sources For provenance purposes, this commit was AI assisted. * fix: parity audit B3-C10 — review + annotate server fixes Review server (B3-B17): - diff/switch missing try/catch error handling - git-add parseBody outside try/catch - feedback missing try/catch error handling - Unknown /api/ai/* paths now return 404 (both Bun and Pi) Annotate server (C1-C10): - Bun annotate server missing pasteApiUrl (short URL sharing broken) - Added pasteApiUrl to Bun options, response, and both hook callers - Pi repoInfo called per-request instead of cached at init - Pi feedback missing try/catch error handling - Missing GET method guards on doc and reference/files For provenance purposes, this commit was AI assisted. * fix: parity audit D3-D5 — draft error handling, editor annotations, resolve-file extraction D3: Pi draft save handler missing error handling — added .catch() with 500 + console.error D4: Pi editor annotation POST missing try/catch — added with "Invalid JSON" 400 D5: Extracted resolveMarkdownFile to packages/shared/resolve-file.ts - Replaced Bun.Glob with runtime-agnostic walkMarkdownFiles (readdirSync) - Made function sync (no longer async) - Pi handleDocRequest now uses shared resolveMarkdownFile instead of inline resolution - Gains Windows path normalization, isWithinProjectRoot security check - Deleted packages/server/resolve-file.ts re-export, consumers import from shared - Cleaned up stale await calls in hook entry, reference handler, and tests - All 19 resolve-file tests pass For provenance purposes, this commit was AI assisted. * fix: parity audit D6-D10 — integrations, PR naming, shared modules D6: Fixed broken detectProjectNameSync — was using require() for non-existent exports. Now uses basename + sanitizeTag directly. D7: Renamed checkAuth → checkPRAuth, getUser → getPRUser across Bun server, hook, and OpenCode plugin to match Pi naming. Also fixed stale resolve-file import in OpenCode plugin. D8-D10: Verified clean — ide, project detection, network. For provenance purposes, this commit was AI assisted. * update openpackage.yml * fix: bump Pi git-add test timeout to 15s for parallel suite stability For provenance purposes, this commit was AI assisted. * test: add route parity test — Bun ↔ Pi server route drift detection For provenance purposes, this commit was AI assisted. * fix(ci): update Pi generate step to use generated/ directory with full file list The Pi extension was refactored to use generated/ subdirectory but the CI generate step still used the old flat layout with a subset of files. For provenance purposes, this commit was AI assisted. * fix(ci): update release workflow Pi generate step to match new layout Same stale generate step as test.yml — old flat layout, missing files. For provenance purposes, this commit was AI assisted. * fix(pi): update files array for modular server layout The files array still referenced the old flat layout (server.ts monolith, root-level generated files, deleted utils.ts). npm publish would have produced a broken package missing server/ and generated/ directories. For provenance purposes, this commit was AI assisted. * feat: add TypeScript type-checking to CI pipeline - Fix broken barrel export: buildFileTree/VaultNode re-exported from @plannotator/shared instead of reference-handlers (P1 bug) - Fix server.port type narrowing in all 3 servers - Fix AI provider type errors (claude-agent-sdk, codex-sdk, opencode-sdk, pi-sdk) - Extract mapPiEvent to pi-events.ts to break Bun→Node type chain - Add tsconfig.json to packages/shared, packages/ai, packages/server, apps/pi-extension - Add `typecheck` script to root package.json - Add type-check step to test.yml and release.yml CI workflows For provenance purposes, this commit was AI assisted. * fix(ci): use bun-types instead of @types/node for typecheck CI environment has bun-types (includes Node types) but not @types/node as a standalone package. For provenance purposes, this commit was AI assisted. * fix(ci): add @types/node for Node-runtime type checks Pi extension and packages/shared run on Node, not Bun — they should type-check against @types/node, not bun-types. Added @types/node as a dev dependency so CI resolves it. For provenance purposes, this commit was AI assisted. * fix: cast Uint8Array.buffer to ArrayBuffer for TS 5.9 compat crypto.subtle.importKey expects BufferSource, but TS 5.9 is stricter about Uint8Array.buffer being ArrayBufferLike (includes SharedArrayBuffer) vs ArrayBuffer. Explicit cast resolves the overload mismatch. Astro pulls in TS 5.9 transitively, so CI resolves a different TypeScript version than local dev. This fix works on both 5.8 and 5.9. For provenance purposes, this commit was AI assisted. * fix(ci): add bun-types as explicit devDependency CI's bun install doesn't hoist bun-types to root node_modules when it's only a transitive dep of @types/bun. Adding it as a direct devDependency guarantees tsc can resolve it. For provenance purposes, this commit was AI assisted. * fix(ci): remove Pi extension from typecheck Pi extension depends on @mariozechner/pi-* peer dependencies that aren't installed in CI. Type-checking it requires Pi's runtime environment. The three packages we check (shared, ai, server) are sufficient to catch barrel export bugs and type errors. Pi extension coverage comes from route parity tests and bun test. For provenance purposes, this commit was AI assisted. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 00:44:54 -07:00
input: string,
projectRoot: string,
): ResolveResult {
const normalizedInput = normalizeMarkdownPathInput(input);
const searchInput = normalizeSeparators(normalizedInput);
const isBareFilename = !searchInput.includes("/");
const targetLookupKey = getLookupKey(searchInput, isBareFilename);
// Restrict to markdown files
if (!isSearchableMarkdownPath(normalizedInput)) {
return { kind: "not_found", input };
}
// 1. Absolute path — use as-is (no project root restriction;
// the user explicitly typed the full path)
if (isAbsoluteMarkdownPath(normalizedInput)) {
const absolutePath = resolveAbsolutePath(normalizedInput);
if (fileExists(absolutePath)) {
return { kind: "found", path: absolutePath };
}
return { kind: "not_found", input };
}
// 2. Exact relative path from project root
const fromRoot = resolve(projectRoot, searchInput);
if (isWithinProjectRoot(fromRoot, projectRoot) && fileExists(fromRoot)) {
return { kind: "found", path: fromRoot };
}
// 3. Case-insensitive search (only scan markdown files)
const allFiles: string[] = [];
walkMarkdownFiles(projectRoot, projectRoot, allFiles, IGNORED_DIRS);
const matches: string[] = [];
for (const match of allFiles) {
const normalizedMatch = normalizeSeparators(match);
const matchLookupKey = getLookupKey(normalizedMatch, isBareFilename);
if (matchLookupKey === targetLookupKey) {
const full = resolve(projectRoot, normalizedMatch);
if (isWithinProjectRoot(full, projectRoot)) {
matches.push(full);
}
}
}
if (matches.length === 1) {
return { kind: "found", path: matches[0] };
}
if (matches.length > 1) {
const projectRootPrefix = `${normalizeComparablePath(projectRoot)}/`;
const relative = matches.map((match) =>
normalizeComparablePath(match).replace(projectRootPrefix, ""),
);
return { kind: "ambiguous", input, matches: relative };
}
return { kind: "not_found", input };
}
feat: support folder annotation via plannotator annotate <folder/> (#389) * feat: add hasMarkdownFiles() and folderPath support for annotate servers Add hasMarkdownFiles() to resolve-file.ts — a shared validation function that checks if a directory contains at least one markdown file, skipping IGNORED_DIRS. Lives alongside the existing walkMarkdownFiles pattern. Both annotate servers (Bun + Pi) accept an optional folderPath that overrides projectRoot in the /api/plan response, and a new "annotate-folder" mode value. For provenance purposes, this commit was AI assisted. * feat: support folder annotation via plannotator annotate <folder/> Wire folder detection through both CLI entry points (Hook + Pi) and the plan editor UI. When a directory is passed instead of a file: - CLI validates markdown files exist via hasMarkdownFiles() - Server starts with empty markdown and mode "annotate-folder" - UI clears demo content, auto-opens sidebar to Files tab - Empty state prompts user to select a file from the tree - Linked doc system handles per-file annotation caching - Feedback exports with per-file headers via existing infrastructure Single-file annotation is unchanged. For provenance purposes, this commit was AI assisted. * fix: address review findings for folder annotation - Include .mdx files in file browser globs (reference-handlers.ts) - Fix imageBaseDir in folder mode — use folder path directly instead of stripping last segment, which produced the parent directory - Guard statSync in Pi extension with try/catch to match hook server pattern For provenance purposes, this commit was AI assisted. * fix: align folder validation exclusions with file browser hasMarkdownFiles() now accepts an excludedDirs parameter. Both CLI entry points pass FILE_BROWSER_EXCLUDED so the preflight check matches the file browser — no more empty sidebar after validation. For provenance purposes, this commit was AI assisted.
2026-03-24 23:37:19 -07:00
/**
* Resolve a markdown file path within a project root.
*
* @param input - User-provided path (absolute, relative, or bare filename)
* @param projectRoot - Project root directory to search within
*/
export function resolveMarkdownFile(
input: string,
projectRoot: string,
): ResolveResult {
const originalInput = input.trim();
const unquotedInput = stripWrappingQuotes(originalInput);
const primary = resolveMarkdownFileCore(unquotedInput, projectRoot);
if (primary.kind === "found") {
return primary;
}
if (primary.kind === "ambiguous") {
return { ...primary, input: originalInput };
}
if (!unquotedInput.startsWith("@")) {
return { kind: "not_found", input: originalInput };
}
const normalizedInput = unquotedInput.replace(/^@+/, "");
if (!normalizedInput) {
return { kind: "not_found", input: originalInput };
}
const fallback = resolveMarkdownFileCore(normalizedInput, projectRoot);
if (fallback.kind === "found") {
return fallback;
}
if (fallback.kind === "ambiguous") {
return { ...fallback, input: originalInput };
}
return { kind: "not_found", input: originalInput };
}
feat: support folder annotation via plannotator annotate <folder/> (#389) * feat: add hasMarkdownFiles() and folderPath support for annotate servers Add hasMarkdownFiles() to resolve-file.ts — a shared validation function that checks if a directory contains at least one markdown file, skipping IGNORED_DIRS. Lives alongside the existing walkMarkdownFiles pattern. Both annotate servers (Bun + Pi) accept an optional folderPath that overrides projectRoot in the /api/plan response, and a new "annotate-folder" mode value. For provenance purposes, this commit was AI assisted. * feat: support folder annotation via plannotator annotate <folder/> Wire folder detection through both CLI entry points (Hook + Pi) and the plan editor UI. When a directory is passed instead of a file: - CLI validates markdown files exist via hasMarkdownFiles() - Server starts with empty markdown and mode "annotate-folder" - UI clears demo content, auto-opens sidebar to Files tab - Empty state prompts user to select a file from the tree - Linked doc system handles per-file annotation caching - Feedback exports with per-file headers via existing infrastructure Single-file annotation is unchanged. For provenance purposes, this commit was AI assisted. * fix: address review findings for folder annotation - Include .mdx files in file browser globs (reference-handlers.ts) - Fix imageBaseDir in folder mode — use folder path directly instead of stripping last segment, which produced the parent directory - Guard statSync in Pi extension with try/catch to match hook server pattern For provenance purposes, this commit was AI assisted. * fix: align folder validation exclusions with file browser hasMarkdownFiles() now accepts an excludedDirs parameter. Both CLI entry points pass FILE_BROWSER_EXCLUDED so the preflight check matches the file browser — no more empty sidebar after validation. For provenance purposes, this commit was AI assisted.
2026-03-24 23:37:19 -07:00
/**
* Check if a directory contains at least one markdown file.
* Used to validate folder annotation targets.
*
* @param dirPath - Directory to search
* @param excludedDirs - Directory names to skip (with trailing slash, e.g. "node_modules/")
*/
export function hasMarkdownFiles(dirPath: string, excludedDirs: string[] = IGNORED_DIRS): boolean {
function walk(dir: string): boolean {
let entries;
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch {
return false;
}
for (const entry of entries) {
if (entry.isDirectory()) {
if (excludedDirs.some((d) => d === entry.name + "/")) continue;
if (walk(join(dir, entry.name))) return true;
} else if (entry.isFile() && /\.mdx?$/i.test(entry.name)) {
return true;
}
}
return false;
}
return walk(dirPath);
}