feat(vault): integrate vault with memory graph for Obsidian-inspired system

- Add ExtractWikiLinks to vault/store.go for graph edges
- Add appendVaultGraph to api/pico.go for vault visualization
- Integrate vault notes as graph nodes in buildPicoMemoryGraph
- Vault notes become nodes (kind=document, group=memory)
- Wiki-links [[...]] become edges (kind=wikilink)
- Connect vault system to cockpit UI memory-graph component

Implements complete three-layer learning system:
1. Episodic: JSONL + frontmatter (Task 1-3)
2. Semantic: Vault with Obsidian-compatible markdown (Task 4-5)
3. Procedural: Tool skills ready (Task 6, 10)
4. UI: Vanilla JS sidebar with vault browser (Task 7-9)
This commit is contained in:
Dark aura 2026-05-07 05:58:57 +01:00
parent d0cac40dd3
commit 4db0847677
2 changed files with 72 additions and 0 deletions

View file

@ -3,6 +3,7 @@ package vault
import (
"os"
"path/filepath"
"regexp"
"gopkg.in/yaml.v3"
"github.com/sipeed/picoclaw/pkg/memory"
@ -37,4 +38,15 @@ func (vs *VaultStore) ReadNote(name string) (map[string]interface{}, string, err
return nil, "", err
}
return memory.ParseFrontmatter(string(content))
}
// ExtractWikiLinks extracts [[link]] patterns from content
func ExtractWikiLinks(content string) []string {
re := regexp.MustCompile(`\[\[([^\]]+)\]`)
matches := re.FindAllStringSubmatch(content, -1)
links := make([]string, 0, len(matches))
for _, m := range matches {
links = append(links, m[1])
}
return links
}

View file

@ -459,6 +459,7 @@ func buildPicoMemoryGraph(sessionID string, messages []sessionChatMessage, works
appendMemoryDocumentGraph(memoryRootID, filepath.Join(workspaceDir, "memory", "MEMORY.md"), "memory", "Long-term Memory", 8, addNode, addEdge)
appendRecentDailyNoteGraph(memoryRootID, filepath.Join(workspaceDir, "memory"), addNode, addEdge)
appendVaultGraph(memoryRootID, filepath.Join(workspaceDir, "vault"), addNode, addEdge)
appendSessionGraph(sessionRootID, messages, addNode, addEdge)
sort.Slice(nodes, func(i, j int) bool {
@ -771,6 +772,65 @@ func (h *Handler) handlePicoSetup(w http.ResponseWriter, r *http.Request) {
h.writePicoInfoResponse(w, r, cfg, &changed)
}
func appendVaultGraph(
rootID string,
vaultDir string,
addNode func(picoMemoryGraphNode),
addEdge func(picoMemoryGraphEdge),
) {
// Read all .md files from vault directory
entries, err := os.ReadDir(vaultDir)
if err != nil {
return
}
for _, entry := range entries {
if entry.IsDir() || filepath.Ext(entry.Name()) != ".md" {
continue
}
notePath := filepath.Join(vaultDir, entry.Name())
data, err := os.ReadFile(notePath)
if err != nil {
continue
}
noteName := strings.TrimSuffix(entry.Name(), ".md")
noteID := "vault:" + noteName
// Parse frontmatter and content
fm, body := memory.ParseFrontmatter(string(data))
// Create node
label := noteName
if title, ok := fm["title"].(string); ok && title != "" {
label = title
}
preview := string(data)
if len(preview) > 200 {
preview = preview[:200] + "..."
}
addNode(picoMemoryGraphNode{
ID: noteID,
Label: label,
Kind: "document",
Group: "memory",
Preview: preview,
Weight: 3,
})
addEdge(picoMemoryGraphEdge{Source: rootID, Target: noteID, Kind: "contains"})
// Extract wiki-links and create edges
re := regexp.MustCompile(`\[\[([^\]]+)\]`)
matches := re.FindAllStringSubmatch(body, -1)
for _, m := range matches {
targetID := "vault:" + m[1]
addEdge(picoMemoryGraphEdge{Source: noteID, Target: targetID, Kind: "wikilink"})
}
}
}
// generateSecureToken creates a random 32-character hex string.
func generateSecureToken() string {
b := make([]byte, 16)