Skip to content
Closed
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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pantry store \

Categories: `decision`, `bug`, `pattern`, `context`, `learning`.

Set `--source` to your agent identifier: `claude-code`, `codex`, `cursor`, or `opencode`.
Set `--source` to your agent identifier: `claude-code`, `codex`, `cursor`, `windsurf`, `antigravity`, or `opencode`.

`--project` defaults to the current directory name — only set it explicitly if storing a note for a different project.

Expand All @@ -72,7 +72,7 @@ You MUST store a note when any of these happen:
Run once to auto-install hooks for your agent:

```bash
pantry setup cursor # or: claude-code, codex, opencode
pantry setup cursor # or: claude-code, windsurf, antigravity, codex, opencode
```

To remove: `pantry uninstall cursor`
Expand Down
26 changes: 20 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Local note storage for coding agents. Your agent keeps notes on decisions, bugs,

## Features

- **Works with multiple agents** — Claude Code, Cursor, Codex, OpenCode, RooCode. One command sets up MCP config for your agent.
- **Works with multiple agents** — Claude Code, Cursor, Windsurf, Antigravity, Codex, OpenCode, RooCode. One command sets up MCP config for your agent.
- **MCP native** — Runs as an MCP server exposing `pantry_store`, `pantry_search`, and `pantry_context` as tools.
- **Local-first** — Everything stays on your machine. Notes are stored as Markdown in `~/.pantry/shelves/`, readable in Obsidian or any editor.
- **Zero idle cost** — No background processes, no daemon, no RAM overhead. The MCP server only runs when the agent starts it.
Expand Down Expand Up @@ -44,7 +44,7 @@ pantry init
### Connect your agent

```bash
pantry setup claude-code # or: cursor, codex, opencode, roocode
pantry setup claude-code # or: cursor, windsurf, antigravity, codex, opencode, roocode
```

This writes the MCP server entry into your agent's config file. Restart the agent and pantry will be available as a tool.
Expand All @@ -53,7 +53,7 @@ Run `pantry doctor` to verify everything is working.

### Tell your agent to use Pantry

MCP registration makes the tools available, but your agent also needs instructions to actually use them. The `setup` command installs a skill file automatically for agents that support it (Claude Code, Cursor, Codex). For other agents — or if you prefer to use a project-level rules file — add the following to your `AGENTS.md`, `.rules`, `CLAUDE.md`, or equivalent:
MCP registration makes the tools available, but your agent also needs instructions to actually use them. The `setup` command installs a skill file automatically for agents that support it (Claude Code, Cursor, Windsurf, Antigravity, Codex). For other agents — or if you prefer to use a project-level rules file — add the following to your `AGENTS.md`, `.rules`, `CLAUDE.md`, or equivalent:

```markdown
## Pantry — persistent notes
Expand All @@ -80,15 +80,18 @@ Do not skip either step. Notes are how context survives across sessions.
Keyword search (FTS5) works with no extra setup. To also enable semantic vector search, configure an embedding provider in `~/.pantry/config.yaml`:

**Ollama (local, free):**

```yaml
embedding:
provider: ollama
model: nomic-embed-text
base_url: http://localhost:11434
```

Install [Ollama](https://ollama.com), then: `ollama pull nomic-embed-text`

**OpenAI:**

```yaml
embedding:
provider: openai
Expand All @@ -97,14 +100,25 @@ embedding:
```

**OpenRouter:**

```yaml
embedding:
provider: openrouter
model: openai/text-embedding-3-small
api_key: sk-or-...
```

**Google (Gemini API):**

```yaml
embedding:
provider: google
model: gemini-embedding-001
api_key: AIzaSy...
```

After changing providers, rebuild the vector index:

```bash
pantry reindex
```
Expand All @@ -116,9 +130,9 @@ All config file values can be overridden with environment variables. They take p
| Variable | Description | Example |
|----------|-------------|---------|
| `PANTRY_HOME` | Override pantry home directory | `/data/pantry` |
| `PANTRY_EMBEDDING_PROVIDER` | Embedding provider | `ollama`, `openai`, `openrouter` |
| `PANTRY_EMBEDDING_MODEL` | Embedding model name | `text-embedding-3-small` |
| `PANTRY_EMBEDDING_API_KEY` | API key for the embedding provider | `sk-...` |
| `PANTRY_EMBEDDING_PROVIDER` | Embedding provider | `ollama`, `openai`, `openrouter`, `google` |
| `PANTRY_EMBEDDING_MODEL` | Embedding model name | `text-embedding-3-small`, `gemini-embedding-001` |
| `PANTRY_EMBEDDING_API_KEY` | API key for the embedding provider | `sk-...`, `AIzaSy...` |
| `PANTRY_EMBEDDING_BASE_URL` | Base URL for the embedding API | `http://localhost:11434` |
| `PANTRY_CONTEXT_SEMANTIC` | Semantic search mode | `auto`, `always`, `never` |

Expand Down
10 changes: 5 additions & 5 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ func LoadConfig(path string) (*Config, error) {
// Validate returns an error if the configuration contains invalid values.
// Call this after LoadConfig to surface misconfiguration at startup.
func (c *Config) Validate() error {
validProviders := map[string]bool{"ollama": true, "openai": true, "openrouter": true}
validProviders := map[string]bool{"ollama": true, "openai": true, "openrouter": true, "google": true}
if !validProviders[c.Embedding.Provider] {
return fmt.Errorf("invalid embedding.provider %q: must be one of ollama, openai, openrouter", c.Embedding.Provider)
return fmt.Errorf("invalid embedding.provider %q: must be one of ollama, openai, openrouter, google", c.Embedding.Provider)
}

if c.Embedding.Model == "" {
Expand All @@ -127,7 +127,7 @@ func (c *Config) Validate() error {
return fmt.Errorf("invalid context.semantic %q: must be one of auto, always, never", c.Context.Semantic)
}

if c.Embedding.Provider == "openai" || c.Embedding.Provider == "openrouter" {
if c.Embedding.Provider == "openai" || c.Embedding.Provider == "openrouter" || c.Embedding.Provider == "google" {
if c.Embedding.APIKey == nil || *c.Embedding.APIKey == "" {
return fmt.Errorf("embedding.api_key is required for provider %q", c.Embedding.Provider)
}
Expand Down Expand Up @@ -162,10 +162,10 @@ func GetDefaultConfigTemplate() string {
# Embedding provider for semantic search.
# Without this, keyword search (FTS5) still works.
embedding:
provider: ollama # ollama | openai | openrouter
provider: ollama # ollama | openai | openrouter | google
model: nomic-embed-text
base_url: http://localhost:11434
# api_key: sk-... # required for openai/openrouter
# api_key: sk-... # required for openai/openrouter/google

# How items are retrieved at session start.
# "auto" uses vectors when available, falls back to keywords.
Expand Down
12 changes: 12 additions & 0 deletions internal/embeddings/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ func NewProvider(cfg config.EmbeddingConfig) (Provider, error) {

return NewOpenAIProvider(cfg.Model, *cfg.APIKey, baseURL), nil

case "google":
if cfg.APIKey == nil || *cfg.APIKey == "" {
return nil, errors.New("API key required for Google provider")
}

baseURL := ""
if cfg.BaseURL != nil {
baseURL = *cfg.BaseURL
}

return NewGoogleProvider(cfg.Model, *cfg.APIKey, baseURL), nil

default:
return nil, fmt.Errorf("unknown embedding provider: %s", cfg.Provider)
}
Expand Down
106 changes: 106 additions & 0 deletions internal/embeddings/google.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package embeddings

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)

// GoogleProvider implements embedding generation using Google's Gemini API.
type GoogleProvider struct {
model string
apiKey string
baseURL string
client *http.Client
}

// NewGoogleProvider creates a new Google Gemini embedding provider.
func NewGoogleProvider(model string, apiKey string, baseURL string) *GoogleProvider {
if baseURL == "" {
baseURL = "https://generativelanguage.googleapis.com/v1beta"
}

return &GoogleProvider{
model: model,
apiKey: apiKey,
baseURL: strings.TrimSuffix(baseURL, "/"),
client: &http.Client{},
}
}

type googleEmbedRequest struct {
Model string `json:"model"`
Content googleEmbedContent `json:"content"`
}

type googleEmbedContent struct {
Parts []googleEmbedPart `json:"parts"`
}

type googleEmbedPart struct {
Text string `json:"text"`
}

type googleEmbedResponse struct {
Embedding struct {
Values []float32 `json:"values"`
} `json:"embedding"`
}

// Embed generates an embedding vector using Google Gemini API.
func (p *GoogleProvider) Embed(ctx context.Context, text string) ([]float32, error) {
// The Gemini API requires the model name to be prefixed with "models/"
modelPath := p.model
if !strings.HasPrefix(modelPath, "models/") {
modelPath = "models/" + modelPath
}

url := fmt.Sprintf("%s/%s:embedContent", p.baseURL, modelPath)

reqData := googleEmbedRequest{
Model: modelPath,
Content: googleEmbedContent{
Parts: []googleEmbedPart{{Text: text}},
},
}

jsonData, err := json.Marshal(reqData)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-goog-api-key", p.apiKey)

resp, err := p.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to call Google API: %w", err)
}

defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("google API returned status %d: %s", resp.StatusCode, string(body))
}

var response googleEmbedResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}

if len(response.Embedding.Values) == 0 {
return nil, fmt.Errorf("no embedding values returned by google api")
}

return response.Embedding.Values, nil
}
Binary file added pantry
Binary file not shown.
Loading