Skip to content

Commit

Permalink
feat: cursor like context in obsidian pipe
Browse files Browse the repository at this point in the history
  • Loading branch information
louis030195 committed Jan 20, 2025
1 parent 8013f7b commit 83ee88f
Show file tree
Hide file tree
Showing 9 changed files with 662 additions and 83 deletions.
Binary file modified pipes/obsidian/bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion pipes/obsidian/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.4",
"@radix-ui/react-tooltip": "^1.1.5",
"@screenpipe/browser": "0.1.17",
"@screenpipe/browser": "0.1.21",
"@screenpipe/js": "1.0.0",
"@shadcn/ui": "^0.0.4",
"@tanstack/react-query": "^5.62.7",
Expand Down
159 changes: 159 additions & 0 deletions pipes/obsidian/src/app/api/files/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { NextResponse } from "next/server";
import path from "path";
import fs from "fs/promises";
import { pipe } from "@screenpipe/js";

// Force Node.js runtime
export const runtime = "nodejs";
export const dynamic = "force-dynamic";

// Cache for vault files - invalidated every 5 minutes
let filesCache: {
files: string[];
vaultPath: string;
timestamp: number;
} | null = null;

const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes

async function findObsidianRoot(startPath: string): Promise<string | null> {
let currentPath = startPath;

while (currentPath !== path.parse(currentPath).root) {
try {
const hasObsidianDir = await fs
.access(path.join(currentPath, ".obsidian"))
.then(() => true)
.catch(() => false);

if (hasObsidianDir) {
return currentPath;
}

currentPath = path.dirname(currentPath);
} catch (error) {
return null;
}
}
return null;
}

async function getAllFiles(vaultPath: string): Promise<string[]> {
// Check cache first
if (
filesCache &&
filesCache.vaultPath === vaultPath &&
Date.now() - filesCache.timestamp < CACHE_DURATION
) {
return filesCache.files;
}

async function getFiles(dir: string): Promise<string[]> {
const entries = await fs.readdir(dir, { withFileTypes: true });
const files = await Promise.all(
entries.map(async (entry) => {
const res = path.resolve(dir, entry.name);
// Skip .obsidian directory
if (entry.isDirectory() && entry.name !== ".obsidian") {
return getFiles(res);
}
return entry.isFile() && entry.name.endsWith(".md") ? res : [];
})
);
return files.flat();
}

const allFiles = await getFiles(vaultPath);
const relativeFiles = allFiles.map((file) => path.relative(vaultPath, file));

// Update cache
filesCache = {
files: relativeFiles,
vaultPath,
timestamp: Date.now(),
};

return relativeFiles;
}

function getSearchScore(file: string, searchTerms: string[]): number {
const lowerFile = file.toLowerCase();
const fileName = path.basename(file).toLowerCase();
let score = 0;

// Exact filename match gets highest score
if (fileName === searchTerms.join(" ").toLowerCase()) {
score += 1000;
}

// Filename contains all terms in order
if (fileName.includes(searchTerms.join(" ").toLowerCase())) {
score += 500;
}

// Individual term matches in filename
for (const term of searchTerms) {
if (fileName.includes(term)) {
score += 100;
}
}

// Path matches
for (const term of searchTerms) {
if (lowerFile.includes(term)) {
score += 10;
}
}

return score;
}

export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const search = searchParams.get("search") || "";
console.log("search term:", search);

const settingsManager = pipe.settings;
if (!settingsManager) {
throw new Error("settingsManager not found");
}

const settings = await settingsManager.getAll();
const initialPath = settings.customSettings?.obsidian?.path;

if (!initialPath) {
return NextResponse.json({ files: [] });
}

const vaultPath = await findObsidianRoot(initialPath);
console.log("vault root path:", vaultPath);

if (!vaultPath) {
return NextResponse.json({ files: [] });
}

const allFiles = await getAllFiles(vaultPath);

// Optimize search with lowercase and pre-split search terms
const searchTerms = search.toLowerCase().split(/\s+/);

const matchingFiles = allFiles
.filter((file) => {
const lowerFile = file.toLowerCase();
return searchTerms.every((term) => lowerFile.includes(term));
})
.map((file) => ({
file,
score: getSearchScore(file, searchTerms),
}))
.sort((a, b) => b.score - a.score) // Sort by score descending
.map(({ file }) => file) // Extract just the filename
.slice(0, 50); // Limit results to 50 files

return NextResponse.json({ files: matchingFiles });
} catch (error) {
console.error("Error fetching files:", error);
return NextResponse.json({ files: [] });
}
}
87 changes: 83 additions & 4 deletions pipes/obsidian/src/app/api/log/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,95 @@ type WorkLog = z.infer<typeof workLog> & {
endTime: string;
};

async function readObsidianFile(filePath: string): Promise<string> {
try {
const content = await fs.readFile(filePath, "utf8");
return content;
} catch (err) {
console.error(`failed to read file ${filePath}:`, err);
return "";
}
}

async function findVaultRoot(startPath: string): Promise<string> {
let currentPath = startPath;

while (currentPath !== "/" && currentPath !== ".") {
try {
// Check if .obsidian exists in current directory
await fs.access(path.join(currentPath, ".obsidian"));
return currentPath; // Found the vault root
} catch {
// Move up one directory
currentPath = path.dirname(currentPath);
}
}
throw new Error("could not find obsidian vault root (.obsidian folder)");
}

async function extractLinkedContent(
prompt: string,
basePath: string
): Promise<string> {
try {
// Find the vault root first
const vaultRoot = await findVaultRoot(basePath);

// Match [[file]] or [[folder/file]] patterns
const linkRegex = /\[\[(.*?)\]\]/g;
const matches = [...prompt.matchAll(linkRegex)];

let enrichedPrompt = prompt;

for (const match of matches) {
const relativePath = match[1];
// Handle .md extension if not present
const fullPath = path.join(
vaultRoot,
relativePath.endsWith(".md") ? relativePath : `${relativePath}.md`
);

try {
const content = await readObsidianFile(fullPath);
// Replace the [[link]] with actual content
enrichedPrompt = enrichedPrompt.replace(
match[0],
`\n--- Content of ${relativePath} ---\n${content}\n---\n`
);
} catch (err) {
console.error(`failed to process link ${relativePath}:`, err);
}
}

return enrichedPrompt;
} catch (err) {
console.error("failed to find vault root:", err);
return prompt; // Return original prompt if we can't process links
}
}

async function generateWorkLog(
screenData: ContentItem[],
model: string,
startTime: Date,
endTime: Date,
customPrompt?: string
customPrompt?: string,
obsidianPath?: string
): Promise<WorkLog> {
let enrichedPrompt = customPrompt || "";

if (customPrompt && obsidianPath) {
enrichedPrompt = await extractLinkedContent(customPrompt, obsidianPath);
}

const defaultPrompt = `Based on the following screen data, generate a concise work activity log entry.
Rules:
- use the screen data to generate the log entry
- focus on describing the activity and tags
User custom prompt: ${customPrompt}
- use the following context to better understand the user's goals and priorities:
${enrichedPrompt}
Screen data: ${JSON.stringify(screenData)}
Return a JSON object with:
Expand All @@ -40,6 +116,8 @@ async function generateWorkLog(
"tags": ["#tag1", "#tag2", "#tag3"]
}`;

console.log("enrichedPrompt prompt:", enrichedPrompt);

const provider = ollama(model);
const response = await generateObject({
model: provider,
Expand Down Expand Up @@ -134,7 +212,8 @@ export async function GET() {
model,
oneHourAgo,
now,
customPrompt
customPrompt,
obsidianPath
);
const _ = await syncLogToObsidian(logEntry, obsidianPath);

Expand Down
1 change: 1 addition & 0 deletions pipes/obsidian/src/app/api/settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export async function GET() {

// Merge with current settings
const rawSettings = await settingsManager.getAll();
console.log("rawSettings", rawSettings);
return NextResponse.json({
...rawSettings,
customSettings: {
Expand Down
Loading

0 comments on commit 83ee88f

Please sign in to comment.