vault backup: 2026-01-08 09:34:22

This commit is contained in:
windyboy
2026-01-08 09:34:22 +08:00
parent e967fa874a
commit 0e4d21f721
28 changed files with 3164 additions and 271 deletions
-6
View File
@@ -1,6 +0,0 @@
{
"activeConversationId": "conv-1767796005456-scja60kdo",
"lastEnvHash": "",
"lastClaudeModel": "sonnet",
"lastCustomModel": ""
}
+310 -152
View File
@@ -21944,12 +21944,155 @@ function formatContextFilesLine(files) {
${files.join(", ")}
</context_files>`;
}
function prependContextFiles(prompt, files) {
return `${formatContextFilesLine(files)}
// === CONTEXT FILE OPTIMIZATION: File summarization ===
async function prependContextFiles(prompt, files, vaultPath = "") {
if (!files || files.length === 0) {
return prompt;
}
const fs = require("fs");
const path = require("path");
const FILE_SIZE_THRESHOLD = 10000; // 10KB
const processedFiles = [];
for (const filePath of files) {
try {
// Resolve absolute path
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.join(vaultPath || process.cwd(), filePath);
// Check file size
const stats = fs.statSync(absolutePath);
if (stats.size > FILE_SIZE_THRESHOLD) {
// Large file: Generate summary
const summary = await generateFileSummary(absolutePath);
processedFiles.push(summary);
} else {
// Small file: Load full content
const content = fs.readFileSync(absolutePath, "utf-8");
processedFiles.push(`📄 **${filePath}**
${content}`);
}
} catch (error) {
// Fallback: Just list the file path
console.error(`[Context File] Failed to process ${filePath}:`, error.message);
processedFiles.push(`📄 **${filePath}** _(Use Read tool to view)_`);
}
}
return `<context_files>
${processedFiles.join("\n\n---\n\n")}
</context_files>
${prompt}`;
}
// Helper: Generate file summary
async function generateFileSummary(filePath) {
const fs = require("fs");
const path = require("path");
const content = fs.readFileSync(filePath, "utf-8");
const fileName = path.basename(filePath);
// Extract YAML frontmatter
const frontmatter = extractYAMLFrontmatter(content);
// Extract markdown headings
const headings = extractMarkdownHeadings(content);
// Extract first paragraph (skip frontmatter)
// Normalize line endings first
const normalized = content.replace(/\r\n/g, "\n");
const textContent = normalized.replace(/^---\n[\s\S]*?\n---\n/, "");
const firstPara = textContent.trim().split("\n\n")[0] || "";
const preview = firstPara.slice(0, 300);
// Format summary
const parts = [];
parts.push(`📄 **${fileName}** _(Summary - ${(content.length / 1024).toFixed(1)}KB)_`);
parts.push("");
if (frontmatter.type || frontmatter.tags || frontmatter.created) {
parts.push("**Metadata:**");
if (frontmatter.type) parts.push(`- Type: ${frontmatter.type}`);
if (frontmatter.tags && frontmatter.tags.length > 0) {
parts.push(`- Tags: ${frontmatter.tags.join(", ")}`);
}
if (frontmatter.created) parts.push(`- Created: ${frontmatter.created}`);
parts.push("");
}
if (headings.length > 0) {
parts.push(`**Structure:** ${headings.slice(0, 5).join(" > ")}`);
if (headings.length > 5) parts.push(`_(+${headings.length - 5} more sections)_`);
parts.push("");
}
if (preview) {
parts.push("**Preview:**");
parts.push(preview + (firstPara.length > 300 ? "..." : ""));
parts.push("");
}
parts.push(`_📖 Use Read tool to view full content: \`${filePath}\`_`);
return parts.join("\n");
}
// Helper: Extract YAML frontmatter
function extractYAMLFrontmatter(content) {
// Normalize line endings to \n
const normalized = content.replace(/\r\n/g, "\n");
const frontmatterMatch = normalized.match(/^---\n([\s\S]*?)\n---/);
if (!frontmatterMatch) return {};
const yaml = frontmatterMatch[1];
const result = {};
// Simple YAML parsing (key: value)
const lines = yaml.split("\n");
for (const line of lines) {
const match = line.match(/^(\w+):\s*(.+)$/);
if (match) {
const [, key, value] = match;
// Handle arrays
if (value.startsWith("[") && value.endsWith("]")) {
result[key] = value
.slice(1, -1)
.split(",")
.map(v => v.trim().replace(/^["']|["']$/g, ""));
} else {
result[key] = value.replace(/^["']|["']$/g, "");
}
}
}
return result;
}
// Helper: Extract markdown headings
function extractMarkdownHeadings(content) {
const headings = [];
// Normalize line endings
const lines = content.replace(/\r\n/g, "\n").split("\n");
for (const line of lines) {
const match = line.match(/^#{1,6}\s+(.+)$/);
if (match) {
headings.push(match[1].trim());
}
}
return headings;
}
// src/utils/env.ts
var path = __toESM(require("path"));
var isWindows = process.platform === "win32";
@@ -22450,7 +22593,7 @@ function formatToolCallForContext(toolCall, maxResultLength = 800) {
const result = truncateToolResult(toolCall.result, maxResultLength);
return `${base} result: ${result}`;
}
function truncateToolResult(result, maxLength = 800) {
function truncateToolResult(result, maxLength = 300) {
if (result.length > maxLength) {
return `${result.slice(0, maxLength)}... (truncated)`;
}
@@ -22462,13 +22605,75 @@ function formatContextLine(message) {
}
return formatCurrentNote(message.currentNote);
}
function buildContextFromHistory(messages) {
// === HISTORY OPTIMIZATION: Token-based message window ===
function buildContextFromHistory(messages, tokenBudget = 4e3) {
var _a, _b, _c;
if (!messages || messages.length === 0) {
return "";
}
// Helper: Estimate tokens for a message (rough approximation: 1 token ≈ 4 characters)
function estimateMessageTokens(message2) {
var _a2;
let chars = 0;
// Content
if (message2.content) {
chars += message2.content.length;
}
// Tool calls
if ((_a2 = message2.toolCalls) == null ? void 0 : _a2.length) {
for (const tc of message2.toolCalls) {
chars += (tc.name || "").length;
chars += JSON.stringify(tc.args || {}).length;
chars += (tc.result || "").length;
}
}
return Math.ceil(chars / 4);
}
const selectedMessages = [];
let currentTokens = 0;
// Always include the first message (initial context)
if (messages.length > 0) {
selectedMessages.push(messages[0]);
currentTokens += estimateMessageTokens(messages[0]);
}
// Add recent messages from newest to oldest, respecting budget
let skipped = 0;
for (let i = messages.length - 1; i >= 1; i--) {
const msg = messages[i];
const msgTokens = estimateMessageTokens(msg);
if (currentTokens + msgTokens > tokenBudget) {
// Budget exceeded, mark remaining as skipped
skipped = i;
break;
}
selectedMessages.splice(1, 0, msg); // Insert after first message
currentTokens += msgTokens;
}
// Format messages
const parts = [];
for (const message of messages) {
// Add skipped indicator
if (skipped > 0) {
parts.push(`[${skipped} earlier messages omitted]`);
}
// Format selected messages
for (const message of selectedMessages) {
if (message.role !== "user" && message.role !== "assistant") {
continue;
}
if (message.role === "assistant") {
const hasContent = message.content && message.content.trim().length > 0;
const hasToolResult = (_a = message.toolCalls) == null ? void 0 : _a.some(
@@ -22478,6 +22683,7 @@ function buildContextFromHistory(messages) {
continue;
}
}
const role = message.role === "user" ? "User" : "Assistant";
const lines = [];
const content = (_b = message.content) == null ? void 0 : _b.trim();
@@ -22486,14 +22692,17 @@ function buildContextFromHistory(messages) {
${content}` : contextLine : content;
lines.push(userPayload ? `${role}: ${userPayload}` : `${role}:`);
if (message.role === "assistant" && ((_c = message.toolCalls) == null ? void 0 : _c.length)) {
const toolLines = message.toolCalls.map((tc) => formatToolCallForContext(tc)).filter(Boolean);
if (toolLines.length > 0) {
lines.push(...toolLines);
}
}
parts.push(lines.join("\n"));
}
return parts.join("\n\n");
}
function getLastUserMessage(messages) {
@@ -23301,149 +23510,31 @@ Vault absolute path: ${vaultPath}` : "";
## Identity & Role
You are **Claudian**, an expert AI assistant specialized in Obsidian vault management, knowledge organization, and code analysis. You operate directly inside the user's Obsidian vault.
You are Claudian, an AI assistant for Obsidian vault management. You understand Markdown, YAML frontmatter, and Wiki-links. Use relative paths from vault root. Never overwrite without context.
**Core Principles:**
1. **Obsidian Native**: You understand Markdown, YAML frontmatter, Wiki-links, and the "second brain" philosophy.
2. **Safety First**: You never overwrite data without understanding context. You always use relative paths.
3. **Proactive Thinking**: You do not just execute; you *plan* and *verify*. You anticipate potential issues (like broken links or missing files).
4. **Clarity**: Your changes are precise, minimizing "noise" in the user's notes or code.
Working directory: vault root.${vaultInfo}
The current working directory is the user's vault root.${vaultInfo}
## Paths
## Critical Path Rules (MUST FOLLOW)
ALL file ops use RELATIVE paths: "notes/file.md" NOT "/notes/file.md". Absolute paths FAIL. Export paths may use ~/absolute.
**ALL file operations** (Read, Write, Edit, Glob, Grep, LS) require RELATIVE paths from vault root:
- \u2713 Correct: "notes/my-note.md", "my-note.md", "folder/subfolder/file.md", "."
- \u2717 WRONG: "/notes/my-note.md", "/my-note.md", "${vaultPath || "/absolute/path"}/file.md"
## Message Format
<current_note>path/to/note.md</current_note>
<query>User question</query>
@filename.md for file mentions.
A leading slash ("/") or absolute path will FAIL. Always use paths relative to the vault root.
## Obsidian
- Files: Markdown (.md), YAML frontmatter, Wiki-links [[note]], Tags #tag
- Dataview queries: don't break them
**Export Exception**: You may write files outside the vault ONLY to configured export paths (write-only). Export destinations may use ~ or absolute paths.
## Tools
## User Message Format
User messages use XML tags for structured context:
\`\`\`xml
<current_note>
path/to/note.md
</current_note>
<query>
User's question or request here
</query>
\`\`\`
- \`<current_note>\`: The note the user is currently viewing/focused on. Read this to understand context. Only appears when the focused note changes.
- \`<query>\`: The user's actual question or request.
- \`@filename.md\`: Files mentioned with @ in the query. Read these files when referenced.
## Obsidian Context
- **Structure**: Files are Markdown (.md). Folders organize content.
- **Frontmatter**: YAML at the top of files (metadata). Respect existing fields.
- **Links**: Internal Wiki-links \`[[note-name]]\` or \`[[folder/note-name]]\`. External links \`[text](url)\`.
- **Tags**: #tag-name for categorization.
- **Dataview**: You may encounter Dataview queries (in \`\`\`dataview\`\`\` blocks). Do not break them unless asked.
- **Vault Config**: \`.obsidian/\` contains internal config. Touch only if you know what you are doing.
## Tool Usage Guidelines
Standard tools (Read, Write, Edit, Glob, Grep, LS, Bash, WebSearch, WebFetch, Skills, AskUserQuestion) work as expected.
**Thinking Process:**
Before taking action, explicitly THINK about:
1. **Context**: Do I have enough information? (Use Read/Search if not).
2. **Impact**: What will this change affect? (Links, other files).
3. **Plan**: What are the steps? (Use TodoWrite for >2 steps).
**Tool-Specific Rules:**
- **Read**:
- Always Read a file before Editing it.
- Read can view images (PNG, JPG, GIF, WebP) for visual analysis.
- **Edit**:
- Requires **EXACT** \`old_string\` match including whitespace/indentation.
- If Edit fails, Read the file again to check the current content.
- **Bash**:
- Runs with vault as working directory.
- **Prefer** Read/Write/Edit over shell commands for file operations (safer).
- Use BashOutput/KillShell to manage background processes.
- **LS**: Uses "." for vault root.
- **WebFetch**: For text/HTML/PDF only. Avoid binaries.
### WebSearch
Use WebSearch strictly according to the following logic:
1. **Static/Historical**: Rely on internal knowledge for established facts, history, or older code libraries.
2. **Dynamic/Recent**: **MUST** search for:
- "Latest" news, versions, docs.
- Events in the current/previous year.
- Volatile data (prices, weather).
3. **Date Awareness**: If user says "yesterday", calculate the date relative to **Current Date**.
4. **Ambiguity**: If unsure if knowledge is outdated, SEARCH.
### Task (Subagents)
Spawn subagents for complex multi-step tasks. Parameters: \`prompt\`, \`description\`, \`subagent_type\`, \`run_in_background\`.
**CRITICAL - Subagent Path Rules:**
- Subagents inherit the vault as their working directory.
- Reference files using **RELATIVE** paths.
- NEVER use absolute paths in subagent prompts.
**When to use:**
- Parallelizable work (main + subagent or multiple subagents)
- Preserve main context budget for sub-tasks
- Offload contained tasks while continuing other work
**Sync Mode (Default - \`run_in_background=false\`)**:
- Runs inline, result returned directly.
- **DEFAULT** to this unless explicitly asked or the task is very long-running.
**Async Mode (\`run_in_background=true\`)**:
- Use ONLY when explicitly requested or task is clearly long-running.
- Returns \`agent_id\` immediately.
- **Must retrieve result** with \`AgentOutputTool\` (poll with block=false, then block=true).
- Never end response without retrieving async results.
**Async workflow:**
1. Launch: \`Task prompt="..." run_in_background=true\` \u2192 get \`agent_id\`
2. Check immediately: \`AgentOutputTool agentId="..." block=false\`
3. Poll while working: \`AgentOutputTool agentId="..." block=false\`
4. When idle: \`AgentOutputTool agentId="..." block=true\` (wait for completion)
5. Report result to user
**Critical:** Never end response without retrieving async task results.
### TodoWrite
Track task progress. Parameter: \`todos\` (array of {content, status, activeForm}).
- Statuses: \`pending\`, \`in_progress\`, \`completed\`
- \`content\`: imperative ("Fix the bug")
- \`activeForm\`: present continuous ("Fixing the bug")
**Use for:** Tasks with 3+ steps, multi-file changes, complex operations.
Use proactively for any task meeting these criteria to keep progress visible.
**Workflow:**
1. **Plan**: Create the todo list at the start.
2. **Execute**: Mark \`in_progress\` -> do work -> Mark \`completed\`.
3. **Update**: If new tasks arise, add them.
**Example:** User asks "refactor auth and add tests"
\`\`\`
[
{content: "Analyze auth module", status: "in_progress", activeForm: "Analyzing auth module"},
{content: "Refactor auth code", status: "pending", activeForm: "Refactoring auth code"},
{content: "Add unit tests", status: "pending", activeForm: "Adding unit tests"}
]
\`\`\`
### Skills
Reusable capability modules. Use the \`Skill\` tool to invoke them when their description matches the user's need.`;
- Read: Always read before editing. Can view images.
- Edit: Requires exact \`old_string\` match.
- Bash: Vault is working dir. Prefer Read/Write/Edit for files.
- Task: Spawn subagents with \`prompt\`, \`subagent_type\`. Use relative paths.
- TodoWrite: Track progress for 3+ step tasks. Format: {content, status, activeForm}.
- Skills: Use \`Skill\` tool when description matches need.`;
}
function getImageInstructions(mediaFolder) {
const folder = mediaFolder.trim();
@@ -23578,18 +23669,82 @@ You are in **plan mode** - a read-only exploration phase before implementation.
**After approval:** The plan is appended to your system prompt and you gain full tool access for implementation.`;
}
function buildSystemPrompt(settings = {}) {
async function buildSystemPrompt(settings = {}) {
var _a, _b;
const path = require("path");
const fs = require("fs");
let prompt = getBaseSystemPrompt(settings.vaultPath);
prompt += getImageInstructions(settings.mediaFolder || "");
// prompt += getImageInstructions(settings.mediaFolder || "");
prompt += getExportInstructions(settings.allowedExportPaths || []);
prompt += getContextPathInstructions(settings.allowedContextPaths || []);
// === TOKEN OPTIMIZATION: Keyword-based instruction retrieval ===
if ((_a = settings.customPrompt) == null ? void 0 : _a.trim()) {
prompt += "\n\n## Custom Instructions\n\n" + settings.customPrompt.trim();
}
if (settings.hasEditorContext) {
prompt += getEditorContextInstructions();
const THRESHOLD = 500; // Characters
const customPrompt = settings.customPrompt.trim();
if (customPrompt.length > THRESHOLD) {
// Large prompt: Use keyword-based retrieval
try {
const userQuery = (settings.userQuery || "").toLowerCase();
const instructionsDir = path.join(settings.vaultPath || ".", ".claude", "memory", "instructions");
// Keyword mapping to instruction files
const keywordMap = {
'safety.md': ['delete', 'remove', 'move', 'file', 'operation', 'approval', 'bulk'],
'organization.md': ['para', 'folder', 'inbox', 'project', 'area', 'resource', 'archive', 'organize', 'structure'],
'linking.md': ['link', 'connect', 'relation', 'backlink', 'reference', 'wiki', 'connection'],
'standards.md': ['frontmatter', 'yaml', 'metadata', 'tag', 'format', 'naming', 'note'],
'git.md': ['git', 'commit', 'push', 'pull', 'backup', 'version']
};
// Detect relevant instruction files
const relevantFiles = [];
for (const [file, keywords] of Object.entries(keywordMap)) {
if (keywords.some(kw => userQuery.includes(kw))) {
relevantFiles.push(file);
}
}
// Default files if no keywords matched
if (relevantFiles.length === 0) {
relevantFiles.push('safety.md', 'organization.md');
}
// Load relevant instructions (max 3 files)
const instructions = [];
for (const file of relevantFiles.slice(0, 3)) {
const filePath = path.join(instructionsDir, file);
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, 'utf-8');
instructions.push(content);
}
}
if (instructions.length > 0) {
prompt += "\n\n## Relevant Custom Instructions\n\n";
prompt += "_(Selected based on query keywords)_\n\n";
prompt += instructions.join('\n\n---\n\n');
} else {
// No instruction files found, use custom prompt
prompt += "\n\n## Custom Instructions\n\n" + customPrompt;
}
} catch (error) {
// Error in retrieval: fallback to direct load
console.error("[Instructions] Retrieval failed, using direct load:", error.message);
prompt += "\n\n## Custom Instructions\n\n" + customPrompt;
}
} else {
// Small prompt: Direct load
prompt += "\n\n## Custom Instructions\n\n" + customPrompt;
}
}
// === END TOKEN OPTIMIZATION ===
// if (settings.hasEditorContext) {
// prompt += getEditorContextInstructions();
// }
if (settings.planMode) {
prompt += getPlanModeInstructions();
}
@@ -24122,12 +24277,13 @@ User: ${prompt}` : historyContext : prompt;
const enhancedPath = getEnhancedPath(customEnv.PATH);
const queryPrompt = this.buildPromptWithImages(prompt, images);
const hasEditorContext = prompt.includes("<editor_selection");
const systemPrompt = buildSystemPrompt({
const systemPrompt = await buildSystemPrompt({
mediaFolder: this.plugin.settings.mediaFolder,
customPrompt: this.plugin.settings.systemPrompt,
allowedExportPaths: this.plugin.settings.allowedExportPaths,
allowedContextPaths: this.plugin.settings.allowedContextPaths,
vaultPath: cwd2,
userQuery: prompt,
hasEditorContext,
planMode: queryOptions == null ? void 0 : queryOptions.planMode,
appendedPlan: (_a = this.approvedPlanContent) != null ? _a : void 0
@@ -30113,7 +30269,7 @@ var InlineEditService = class {
/** Edits text according to instructions (initial request). */
async editText(request) {
this.sessionId = null;
const prompt = this.buildPrompt(request);
const prompt = await this.buildPrompt(request);
return this.sendMessage(prompt);
}
/** Continues conversation with a follow-up message. */
@@ -30123,7 +30279,8 @@ var InlineEditService = class {
}
let prompt = message;
if (contextFiles && contextFiles.length > 0) {
prompt = prependContextFiles(message, contextFiles);
const vaultPath = getVaultPath(this.plugin.app);
prompt = await prependContextFiles(message, contextFiles, vaultPath);
}
return this.sendMessage(prompt);
}
@@ -30211,7 +30368,7 @@ var InlineEditService = class {
}
return { success: false, error: "Empty response" };
}
buildPrompt(request) {
async buildPrompt(request) {
let prompt;
if (request.mode === "cursor") {
prompt = this.buildCursorPrompt(request);
@@ -30228,7 +30385,8 @@ var InlineEditService = class {
].join("\n");
}
if (request.contextFiles && request.contextFiles.length > 0) {
prompt = prependContextFiles(prompt, request.contextFiles);
const vaultPath = getVaultPath(this.plugin.app);
prompt = await prependContextFiles(prompt, request.contextFiles, vaultPath);
}
return prompt;
}