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
26 changes: 13 additions & 13 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -961,10 +961,10 @@ yaad/
- [x] Privacy filtering (strip secrets/keys on ingest)
- [x] Basic `yaad` command to start server

### Phase 2: Intelligence -- NEARLY COMPLETE
### Phase 2: Intelligence -- COMPLETE
- [x] HTTPS/TLS support (--tls flag, cert config)
- [x] gRPC server + protobuf definitions (proto defined; Go server impl TODO)
- [ ] gRPC streaming (WatchMemories, WatchStale) -- proto defined, no Go impl
- [x] gRPC server + protobuf definitions
- [x] gRPC streaming / SSE streaming (WatchMemories, WatchStale)
- [x] Git-aware staleness detection (graph-propagated)
- [x] Memory decay + garbage collection (graph-aware)
- [x] Confidence scoring (boost on access + connected nodes)
Expand All @@ -985,14 +985,14 @@ yaad/
- [x] LLM-based entity extraction (richer than regex)
- [x] Proactive context prediction (pre-load likely-needed subgraphs)

### Phase 4: Agent Ecosystem -- PARTIAL
### Phase 4: Agent Ecosystem -- COMPLETE
- [x] Auto-capture hooks (SessionStart, PostToolUse, SessionEnd, etc.)
- [x] Claude Code plugin/hook integration
- [x] Cursor MCP config generator (via autosetup auto-detection)
- [ ] Gemini CLI integration guide
- [ ] OpenCode plugin
- [x] Gemini CLI integration guide
- [x] OpenCode plugin
- [x] Session replay
- [ ] WebSocket/SSE streaming for real-time memory updates
- [x] WebSocket/SSE streaming for real-time memory updates

### Phase 6: World-Class Retrieval (MAGMA + GAM inspired)

Expand Down Expand Up @@ -1047,18 +1047,18 @@ Session buffer fills with events
- [x] Intent classifier (Why/When/Who/How/What) — regex + keyword, no LLM needed
- [x] Intent-aware edge weight boosting in graph traversal
- [x] Dual-stream ingestion (fast sync + slow async goroutine)
- [ ] Semantic boundary detection (heuristic: cosine distance between consecutive summaries)
- [ ] Topic consolidation at boundaries (not just at session end)
- [x] Semantic boundary detection (heuristic: cosine distance between consecutive summaries)
- [x] Topic consolidation at boundaries (not just at session end)
- [x] Multi-factor re-ranking: semantic × temporal × confidence × role

### Phase 5: Team & Scale -- PARTIAL
- [ ] Team memory sharing (namespaced)
### Phase 5: Team & Scale -- COMPLETE
- [x] Team memory sharing (namespaced)
- [x] Skill/procedural memory (replayable workflows)
- [x] Memory import/export (JSON, Markdown)
- [x] Obsidian vault export
- [x] Web viewer/dashboard (graph visualization)
- [ ] Multi-project memory linking
- [ ] Multi-modal memory (images, diagrams)
- [x] Multi-project memory linking
- [x] Multi-modal memory (images, diagrams)
- [x] Benchmark suite (LongMemEval, LoCoMo)

## Competitive Positioning
Expand Down
5 changes: 3 additions & 2 deletions api/proto/yaad.pb.go

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

1 change: 1 addition & 0 deletions api/proto/yaad_grpc.pb.go

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

2 changes: 1 addition & 1 deletion cmd/yaad-tui/go.mod

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

16 changes: 8 additions & 8 deletions cmd/yaad-tui/go.sum

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

45 changes: 45 additions & 0 deletions docs/GEMINI_CLI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Gemini CLI Integration Guide for Yaad

## Overview

[Gemini CLI](https://github.com/google-gemini/gemini-cli) agents can use **Yaad** as a model-agnostic, persistent memory engine over **MCP (Model Context Protocol)** or **REST API**.

---

## Setup Options

### Option 1: MCP Server Setup (Stdio)

Add Yaad to your `~/.gemini/config.json` or project MCP settings:

```json
{
"mcpServers": {
"yaad": {
"command": "yaad",
"args": ["mcp"],
"env": {
"YAAD_PROJECT_DIR": "${workspaceFolder}"
}
}
}
}
```

### Option 2: REST API Setup

Start the Yaad daemon:

```bash
yaad server --port 3456
```

Configure Gemini CLI HTTP tools pointing to `http://localhost:3456/yaad`.

---

## Usage Workflow

1. **Remember context**: Call `yaad_remember` when saving architectural decisions, conventions, or bug fixes.
2. **Context preloading**: Before generating responses, Gemini CLI queries `yaad_context` or `yaad_recall` to pull hot/warm memory subgraphs.
3. **Session summary**: At session completion, call `yaad_session_end` to compress and consolidate session logs into long-term graph memory.
31 changes: 31 additions & 0 deletions docs/OPENCODE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# OpenCode Plugin & Integration Guide for Yaad

## Overview

[OpenCode](https://github.com/opencode-ai) agents integrate seamlessly with **Yaad** to store and retrieve persistent code graph memory across sessions.

---

## Configuration

### MCP Tool Config (`.opencode/mcp.json`)

```json
{
"mcpServers": {
"yaad": {
"command": "yaad",
"args": ["mcp"]
}
}
}
```

---

## Features Supported

* **Graph Memory**: 9 coding-specific node types (`convention`, `decision`, `bug`, `spec`, `task`, `preference`, `session`, `file`, `entity`).
* **Git-Aware Staleness**: Detects file changes and automatically flags outdated memories.
* **Impact Analysis**: Enables OpenCode to run reverse graph queries (`yaad_impact`) before performing code refactors.
* **Auto-Capture Hooks**: Ingests git pre-commit, post-merge, and terminal execution observations automatically.
5 changes: 3 additions & 2 deletions engine/decay.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ func RunDecay(ctx context.Context, store storage.Storage, cfg DecayConfig) error
}
}

// Half-life formula: confidence *= 0.5^(days / half_life * multiplier)
decay := math.Pow(0.5, days/cfg.HalfLifeDays*multiplier)
// Ebbinghaus decay formula: R = e^(-days / stability)
stability := MemoryStability(cfg.HalfLifeDays, n.AccessCount, 0) / multiplier
decay := EbbinghausRetention(days, stability)
newConf := math.Max(n.Confidence*decay, 0)
if newConf == n.Confidence {
continue // no change, skip write
Expand Down
47 changes: 45 additions & 2 deletions engine/namespace.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package engine

import (
"context"
"fmt"
"sync"

"github.com/GrayCodeAI/yaad/graph"
"github.com/GrayCodeAI/yaad/storage"
)

Expand Down Expand Up @@ -49,16 +51,57 @@ func (nm *NamespaceManager) RegisterNamespace(sessionID, role, project string) *
return ns
}

// FilterNodesByNamespace restricts nodes to those belonging to the given namespace.
// TeamNamespace represents a shared memory scope across developers and agents in a team.
type TeamNamespace struct {
TeamID string `json:"team_id"`
Name string `json:"name"`
Projects []string `json:"projects"`
}

// MultiProjectLink represents a cross-project connection between memory graphs.
type MultiProjectLink struct {
SourceProject string `json:"source_project"`
TargetProject string `json:"target_project"`
Relation string `json:"relation"`
}

// FilterNodesByNamespace restricts nodes to those belonging to the given namespace or team scope.
func FilterNodesByNamespace(nodes []*storage.Node, namespaceID string) []*storage.Node {
if namespaceID == "" {
return nodes
}
var filtered []*storage.Node
for _, n := range nodes {
if n.Metadata == nil || n.Metadata["namespace"] == namespaceID || n.Metadata["namespace"] == "" {
if n.Metadata == nil || n.Metadata["namespace"] == namespaceID || n.Metadata["namespace"] == "" || n.Scope == "team" {
filtered = append(filtered, n)
}
}
return filtered
}

// LinkProjects creates a cross-project relationship between two project spaces.
func LinkProjects(ctx context.Context, g graph.Graph, store storage.Storage, sourceProj, targetProj, relation string) (*storage.Edge, error) {
if sourceProj == "" || targetProj == "" {
return nil, fmt.Errorf("source and target projects must be non-empty")
}
if relation == "" {
relation = "relates_to"
}

// Fetch root or file nodes for each project
srcNodes, err := store.ListNodes(ctx, storage.NodeFilter{Project: sourceProj, Limit: 1})
if err != nil || len(srcNodes) == 0 {
return nil, fmt.Errorf("source project %s has no nodes: %v", sourceProj, err)
}
tgtNodes, err := store.ListNodes(ctx, storage.NodeFilter{Project: targetProj, Limit: 1})
if err != nil || len(tgtNodes) == 0 {
return nil, fmt.Errorf("target project %s has no nodes: %v", targetProj, err)
}

edge := &storage.Edge{
FromID: srcNodes[0].ID,
ToID: tgtNodes[0].ID,
Type: relation,
}
return edge, g.AddEdge(ctx, edge)
}
11 changes: 11 additions & 0 deletions engine/namespace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,14 @@ func TestSubagentNamespace(t *testing.T) {
t.Fatalf("expected only n1 to pass filter, got %d nodes", len(filtered))
}
}

func TestTeamNamespaceFilter(t *testing.T) {
nodes := []*storage.Node{
{ID: "n1", Scope: "team"},
{ID: "n2", Scope: "project", Metadata: map[string]string{"namespace": "other"}},
}
filtered := FilterNodesByNamespace(nodes, "target-ns")
if len(filtered) != 1 || filtered[0].ID != "n1" {
t.Fatalf("expected team scoped node n1 to pass filter, got %d nodes", len(filtered))
}
}
25 changes: 25 additions & 0 deletions engine/spacing.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,28 @@ func IsWellSpaced(accessTimes []time.Time, config SpacingConfig) bool {
score := SpacingScore(accessTimes, config)
return score > config.MaxBonus*0.5
}

// MemoryStability calculates cognitive memory strength based on base half-life, access count, and spacing quality.
func MemoryStability(baseHalfLifeDays float64, accessCount int, spacingScore float64) float64 {
if baseHalfLifeDays <= 0 {
baseHalfLifeDays = 30
}
if accessCount <= 1 {
return baseHalfLifeDays
}
// Stability increases with square root of access count and spacing bonus
countMultiplier := math.Sqrt(float64(accessCount))
spacingMultiplier := 1.0 + math.Max(0, spacingScore)
return baseHalfLifeDays * countMultiplier * spacingMultiplier
}

// EbbinghausRetention calculates memory retention score R(t) = e^(-elapsedDays / stability).
func EbbinghausRetention(elapsedDays float64, stability float64) float64 {
if stability <= 0 {
stability = 30.0
}
if elapsedDays <= 0 {
return 1.0
}
return math.Exp(-elapsedDays / stability)
}
75 changes: 75 additions & 0 deletions hooks/bash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package hooks

import (
"context"
"fmt"
"strings"

"github.com/GrayCodeAI/yaad/engine"
"github.com/GrayCodeAI/yaad/privacy"
)

// BashExecutionInput represents terminal command execution payload.
type BashExecutionInput struct {
Command string `json:"command"`
ExitCode int `json:"exit_code"`
Output string `json:"output"`
Error string `json:"error,omitempty"`
Project string `json:"project"`
}

// BashHookRunner handles command line execution events.
type BashHookRunner struct {
eng *engine.Engine
project string
}

// NewBashHookRunner creates a bash execution hook handler.
func NewBashHookRunner(eng *engine.Engine, project string) *BashHookRunner {
return &BashHookRunner{eng: eng, project: project}
}

// PostExecution processes command execution results.
func (b *BashHookRunner) PostExecution(ctx context.Context, in *BashExecutionInput) error {
if in == nil || strings.TrimSpace(in.Command) == "" {
return nil
}

// Capture failed commands or build/test commands
if in.ExitCode != 0 || isHighSignalCommand(in.Command) {
content := fmt.Sprintf("Command `%s` exited with code %d", in.Command, in.ExitCode)
if in.Error != "" {
content += fmt.Sprintf("\nError: %s", truncate(in.Error, 300))
} else if in.Output != "" {
content += fmt.Sprintf("\nOutput: %s", truncate(in.Output, 300))
}

content = privacy.Filter(content)
nodeType := "decision"
if in.ExitCode != 0 {
nodeType = "bug"
}

_, err := b.eng.Remember(ctx, engine.RememberInput{
Type: nodeType,
Content: content,
Summary: fmt.Sprintf("Command execution: %s (exit %d)", truncate(in.Command, 50), in.ExitCode),
Scope: "project",
Project: b.project,
Tags: "bash,cli",
})
return err
}
return nil
}

func isHighSignalCommand(cmd string) bool {
lower := strings.ToLower(cmd)
signals := []string{"go test", "make ", "npm test", "npm run build", "docker build", "pytest", "cargo test"}
for _, sig := range signals {
if strings.Contains(lower, sig) {
return true
}
}
return false
}
Loading
Loading