# Portfolio Manager
<p align="center">
<img src="assets/portfolio_manager.svg" alt="Portfolio Manager Logo" width="200">
</p>
<p align="center">
<a href="https://hex.pm/packages/portfolio_manager"><img alt="Hex.pm" src="https://img.shields.io/hexpm/v/portfolio_manager.svg"></a>
<a href="https://hexdocs.pm/portfolio_manager"><img alt="Documentation" src="https://img.shields.io/badge/docs-hexdocs-purple.svg"></a>
<a href="https://github.com/nshkrdotcom/portfolio_manager/actions"><img alt="Build Status" src="https://img.shields.io/github/actions/workflow/status/nshkrdotcom/portfolio_manager/ci.yml"></a>
<a href="https://opensource.org/licenses/MIT"><img alt="License" src="https://img.shields.io/hexpm/l/portfolio_manager.svg"></a>
</p>
**AI-native personal project intelligence system - manage, track, and search across all your repositories with semantic understanding and agentic capabilities.**
---
## What Is This?
Portfolio Manager is a pure Elixir library for tracking and managing context about all your software projects. It provides:
- **Registry**: Catalog of all repos you manage across multiple accounts
- **Context**: Structured metadata, notes, and decisions per repo
- **Relationships**: How repos connect (dependencies, ports, forks)
- **Detection**: Auto-detect repo type, language, and purpose
- **Semantic Search**: Vector embeddings via `gemini_ex` for intelligent querying
- **Agentic Queries**: Multi-step reasoning with tool use (search, analyze, compare)
- **Multi-LLM**: Works with Gemini, Codex, and Claude (any combination)
- **Workflows**: Automated tasks (port sync, doc generation, health checks)
All structured data is stored in a private git repository for versioning and backup.
Local-only state (cache, review queue, REPL history) lives in `.portfolio/` and is gitignored.
### RAG Integration
Portfolio Manager integrates with an enhanced RAG system providing:
```
┌───────────────────────────────────────────────────────────┐
│ Portfolio Manager │
│ semantic_search/3 | query/3 (agentic) | chat/3 │
└───────────────────────────────────────────────────────────┘
│
┌───────────────────────────┴───────────────────────────────┐
│ RAG Layer │
│ Embeddings: gemini_ex LLMs: gemini/codex/claude │
│ Search: Torus + pgvector Agent: Tools + Memory │
└───────────────────────────────────────────────────────────┘
```
## Installation
Add `portfolio_manager` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:portfolio_manager, "~> 0.1.0"}
]
end
```
Then run:
```bash
mix deps.get
```
## Quick Start
### Using the CLI
```bash
# Initialize a portfolio
mix portfolio.init ~/my-portfolio
# Scan directories for repositories (defaults to config.yml)
mix portfolio.scan
mix portfolio.scan ~/projects ~/work
mix portfolio.scan --agentic --review
# List tracked repos
mix portfolio.list
mix portfolio.list --status=active --type=library
# Show repo details
mix portfolio.show my-project
# Add/remove repositories
mix portfolio.add ~/projects/new-repo
mix portfolio.remove old-repo
# Edit repository metadata
mix portfolio.edit my-project --type=library --status=active
mix portfolio.edit my-project --add-tag=elixir --add-tag=ai
# Search across repos
mix portfolio.search authentication
# AI-powered queries (requires GOOGLE_API_KEY)
mix portfolio.ask "which repos use phoenix?"
# Show portfolio status
mix portfolio.status
# Graph and review agentic detections
mix portfolio.graph
mix portfolio.review --accept-all --threshold=0.9
# Sync portfolio state
mix portfolio.sync
mix portfolio.sync --full --views
mix portfolio.sync --computed-only --check-remotes
# Run workflows
mix portfolio.run --list
mix portfolio.run health-check --repo=my-project
mix portfolio.run port-sync --dry-run
# Configuration management
mix portfolio.config show
mix portfolio.config set sync.auto_commit true
mix portfolio.config add-dir ~/work
# Shell completion (bash/zsh/fish)
mix portfolio.completion --shell=bash >> ~/.bashrc
# Interactive REPL mode
mix portfolio.repl
```
### Using the Elixir API
```elixir
# Initialize with a portfolio repo path
{:ok, portfolio} = PortfolioManager.init("~/portfolio")
# Scan directories for repos
{:ok, discovered} = PortfolioManager.scan(portfolio, ["~/projects", "~/work"])
# List all tracked repos
repos = PortfolioManager.list_repos(portfolio)
# Get detailed context for a repo
{:ok, context} = PortfolioManager.get_context(portfolio, "my-project")
# Search across all repos
results = PortfolioManager.search(portfolio, "authentication")
# Semantic search (requires embeddings)
results = PortfolioManager.semantic_search(portfolio, "error handling patterns")
# Generate computed views
:ok = PortfolioManager.generate_views(portfolio)
```
## Guides
- [01 Overview and Concepts](guides/01_overview.md) - mental model and vocabulary
- [02 Installation and Initialization](guides/02_installation_and_init.md) - install and bootstrap
- [03 Configuration and Structure](guides/03_configuration_and_structure.md) - config and layout
- [04 CLI Reference](guides/04_cli_reference.md) - command-by-command guide
- [05 Detection and Metadata](guides/05_detection_and_metadata.md) - detection sources and computed fields
- [06 Agentic Detection and Review](guides/06_agentic_detection_and_review.md) - LLM detection and review queue
- [07 Views and Graph](guides/07_views_and_graph.md) - computed views and relationship graphs
- [08 Workflows](guides/08_workflows.md) - YAML workflows and step types
- [09 Search and Cache](guides/09_search_and_cache.md) - search modes and SQLite cache
- [10 Library API](guides/10_library_api.md) - API overview and adapters
- [11 Operations and Migration](guides/11_operations_and_migration.md) - centralized operations
## Configuration
Portfolio Manager uses a configuration file at the root of your portfolio repo.
### Default Portfolio Location
By default, the portfolio repo is expected at `~/portfolio`. Override this with `PORTFOLIO_DIR` or app config:
```bash
export PORTFOLIO_DIR=~/p/g/n/portfolio
```
```elixir
# In config/config.exs
config :portfolio_manager,
portfolio_path: "~/my-portfolio"
```
### Environment Variables
```bash
# Portfolio location override
export PORTFOLIO_DIR=~/p/g/n/portfolio
# Required for embeddings
export GOOGLE_API_KEY="your-gemini-api-key"
# Or for OpenAI
export OPENAI_API_KEY="your-openai-key"
```
### Portfolio Config File
The portfolio repo itself contains a `config.yml`:
```yaml
# ~/portfolio/config.yml
version: "1.0"
scan:
directories:
- ~/projects
- ~/work
exclude_patterns:
- "**/node_modules/**"
- "**/.git/**"
agents:
enabled: true
auto_detect: true
```
## API Reference
### Core Functions
```elixir
# Initialization
PortfolioManager.init(path) # Initialize/load portfolio
PortfolioManager.init() # Use PORTFOLIO_DIR or ~/portfolio
# Discovery
PortfolioManager.scan(portfolio, dirs) # Scan directories for repos
PortfolioManager.add(portfolio, path) # Add single repo
PortfolioManager.remove(portfolio, id) # Remove repo from tracking
# Querying
PortfolioManager.list_repos(portfolio, opts \\ [])
PortfolioManager.get_repo(portfolio, id)
PortfolioManager.get_context(portfolio, id)
PortfolioManager.search(portfolio, query, opts \\ [])
PortfolioManager.semantic_search(portfolio, query, opts \\ [])
# Relationships
PortfolioManager.get_relationships(portfolio, id)
PortfolioManager.add_relationship(portfolio, from, to, type)
# Context Management
PortfolioManager.update_context(portfolio, id, updates)
PortfolioManager.add_note(portfolio, id, content)
PortfolioManager.add_decision(portfolio, id, title, content)
# Status
PortfolioManager.status(portfolio)
PortfolioManager.sync(portfolio)
```
### Repo Types
```elixir
:library # Reusable library/package
:application # Standalone application
:port # Port of another library
:fork # Fork of another repo
:experiment # Experimental/learning project
:template # Project template
:config # Configuration repo
:docs # Documentation repo
```
### Relationship Types
```elixir
:depends_on # A depends on B
:port_of # A is a port of B
:fork_of # A is a fork of B
:evolved_from # A evolved from B
:related_to # General relationship
```
## Architecture
Portfolio Manager uses hexagonal architecture:
```
┌───────────────────────────────────────────────────────────┐
│ APPLICATION │
│ PortfolioManager (main API) │
└───────────────────────────────────────────────────────────┘
│
┌─────────────────────────────┴─────────────────────────────┐
│ DOMAIN │
│ Repo | Context | Relationship | Detection | Search │
└───────────────────────────────────────────────────────────┘
│
┌─────────────────────────────┴─────────────────────────────┐
│ PORTS │
│ StoragePort | GitPort | EmbeddingPort | DetectionPort │
└───────────────────────────────────────────────────────────┘
│
┌─────────────────────────────┴─────────────────────────────┐
│ ADAPTERS │
│ YAMLStorage | LocalGit | GeminiEmbedding | FileDetector │
└───────────────────────────────────────────────────────────┘
```
## Examples
### Track a Port Repository
```elixir
# Add a repo that's a port of an upstream library
{:ok, _} = PortfolioManager.add(portfolio, "~/projects/instructor_ex")
# Set it as a port
PortfolioManager.update_context(portfolio, "instructor_ex", %{
type: :port,
port: %{
upstream_url: "https://github.com/instructor-ai/instructor",
upstream_language: "python",
coverage: "partial"
}
})
# Add relationship
PortfolioManager.add_relationship(
portfolio,
"instructor_ex",
"instructor-ai/instructor",
:port_of
)
```
### Query by Criteria
```elixir
# Find all active Elixir libraries
PortfolioManager.list_repos(portfolio,
status: :active,
type: :library,
language: :elixir
)
# Find stale repos
PortfolioManager.list_repos(portfolio,
status: :stale
)
# Find repos by tag
PortfolioManager.list_repos(portfolio,
tags: ["ai", "ml"]
)
```
### Semantic Search
```elixir
# Search for repos related to a concept
results = PortfolioManager.semantic_search(portfolio,
"distributed task processing"
)
# Returns ranked results with context
Enum.each(results, fn %{repo_id: id, score: score, snippet: snippet} ->
IO.puts("#{id} (#{Float.round(score, 2)}): #{snippet}")
end)
```
### Agentic Queries
```elixir
# Ask complex questions - the agent uses tools to find answers
{:ok, result} = PortfolioManager.query(portfolio,
"Compare my instructor_ex port to the upstream Python version"
)
# Agent: searches repos → gets context → compares → synthesizes answer
IO.puts(result.answer)
# "Key differences between instructor_ex and upstream:
# 1. Uses Ecto for validation instead of Pydantic
# 2. Streaming implementation differs..."
IO.inspect(result.tools_used)
# [:search_repos, :get_repo_context, :find_relationships, :compare_repos]
```
### Interactive Sessions
```elixir
# Multi-turn conversation with memory
{:ok, session} = PortfolioManager.start_session(portfolio)
{:ok, _} = PortfolioManager.chat(session, "Show me all my Elixir libraries")
{:ok, _} = PortfolioManager.chat(session, "Which ones are ports?")
{:ok, _} = PortfolioManager.chat(session, "Tell me more about the second one")
# Session remembers context from previous messages
```
### Workflows
Define automated multi-step tasks in YAML:
```yaml
# ~/portfolio/workflows/my-workflow.yml
schema_version: 1
workflow:
id: my-workflow
name: "My Workflow"
description: Custom workflow example
steps:
- id: check-status
type: git
action: status
- id: run-tests
type: shell
action: run
inputs:
command: mix test
- id: analyze
type: agent
action: analyze
inputs:
prompt: "Analyze the test results and suggest improvements"
```
Run workflows:
```bash
mix portfolio.run my-workflow --repo=my-project
mix portfolio.run --list # Show available workflows
```
### Relationship Graph
Visualize repository dependencies:
```elixir
# Build graph from portfolio
graph = PortfolioManager.Graph.build(portfolio)
# ASCII visualization
IO.puts(PortfolioManager.Graph.to_ascii(graph))
# Export to Graphviz DOT format
File.write!("portfolio.dot", PortfolioManager.Graph.to_dot(graph))
# Find path between repos
path = PortfolioManager.Graph.find_path(graph, "app", "core-lib")
# Detect cycles
cycles = PortfolioManager.Graph.find_cycles(graph)
# Topological sort (for build order)
{:ok, order} = PortfolioManager.Graph.topo_sort(graph)
```
### SQLite Cache (Optional)
For large portfolios, enable SQLite caching for faster queries:
```elixir
# Add exqlite to your deps (it's optional)
{:exqlite, "~> 0.23"}
# Start the cache
{:ok, cache} = PortfolioManager.Cache.SQLite.start_link(
portfolio_path: "~/portfolio"
)
# Sync repos to cache
PortfolioManager.Cache.SQLite.sync(cache, repos)
# Fast indexed queries
{:ok, results} = PortfolioManager.Cache.SQLite.filter(cache,
language: "elixir",
status: :active
)
```
## Development
```bash
# Clone the repo
git clone https://github.com/nshkrdotcom/portfolio_manager
cd portfolio_manager
# Install dependencies
mix deps.get
# Run tests
mix test
# Run with coverage
mix test --cover
# Generate docs
mix docs
```
## Testing
This library uses [Supertester](https://github.com/nshkrdotcom/supertester) for robust OTP testing:
```elixir
# All tests run with async: true
use Supertester.ExUnitFoundation, isolation: :full_isolation
# Deterministic async testing
:ok = cast_and_sync(server, :operation)
assert_genserver_state(server, fn state -> state.count == 1 end)
```
## Related Projects
**Core Infrastructure**:
- [portfolio](https://github.com/nshkrdotcom/portfolio) - Private portfolio data repository (example)
- [rag](https://github.com/nshkrdotcom/rag) - Enhanced RAG library (fork of Bitcrowd)
- [supertester](https://github.com/nshkrdotcom/supertester) - OTP testing toolkit
**LLM Providers** (any combination works):
- [gemini_ex](https://github.com/nshkrdotcom/gemini_ex) - Elixir client for Google Gemini API (embeddings + LLM)
- [codex_sdk](https://github.com/nshkrdotcom/codex_sdk) - OpenAI Codex SDK for Elixir
- [claude_agent_sdk](https://github.com/nshkrdotcom/claude_agent_sdk) - Claude Agent SDK for Elixir
**Search**:
- [Torus](https://github.com/dimamik/torus) - PostgreSQL search integration for Ecto
## License
MIT License - see [LICENSE](LICENSE) for details.
---
<p align="center">
Built with care by <a href="https://github.com/nshkrdotcom">nshkrdotcom</a>
</p>