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
28 changes: 22 additions & 6 deletions docs/guides/0012-cli_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,33 @@ If no subcommand is given, the CLI prints "Missing command." and shows the main

## Global Options

These options can be used before any subcommand (and some are also available per-subcommand where noted).
These options belong to the root command. **`--env-file` / `-f` and `--verbose` / `-v` must appear before the subcommand name** (standard Click group options). **`--json` / `-j` is also accepted on many subcommands** after the subcommand—see each command’s help.

| Option | Short | Description |
|--------|-------|-------------|
| `--env-file PATH` | `-e` | Path to a `.env` configuration file. Overrides the default (e.g. `./.env`). |
| `--env-file PATH` | `-f` | Path to a `.env` configuration file. Overrides the default (e.g. `./.env`). |
| `--json` | `-j` | Output results in JSON format. |
| `--verbose` | `-v` | Enable verbose output (e.g. stack traces on errors). |
| `--install-completion SHELL` | — | Install shell completion for `bash`, `zsh`, `fish`, or `powershell`. |
| `--version` | — | Show CLI version. |
| `--help` | `-h` | Show help. |

### Global env file placement

Options **`--env-file` / `-f`** at the root:

- **Placement:** Always **before** the subcommand, e.g. `pmem -f ./.env.staging memory list`, not `pmem memory list -f ./.env` (the latter is invalid for the global option).
- **Scope:** The same file is used for **`memory`**, **`config`**, **`stats`**, **`manage`**, and **`shell`** for that invocation (same behavior as setting `POWERMEM_ENV_FILE`).
- **`config validate` and `config init`** also accept `--env-file` / `-f` **on the subcommand**:
- **validate:** file to validate (if omitted, falls back to the global `--env-file`, then the default discovered `.env`).
- **init:** target file to write (if omitted, falls back to the global `--env-file`, then the default path).
- **`memory search`:** After `search`, **`-f` means `--filters`** (JSON filters), not the env file. To point at a `.env` for that run, use **`pmem -f path/to/.env memory search "…"`** or **`pmem --env-file path/to/.env memory search "…"`**.

**Examples:**

```bash
pmem -e .env.production memory list
pmem -f .env.production memory list
pmem --env-file .env.production config show
pmem --json stats
pmem -v memory add "User prefers dark mode" --user-id user123
pmem --install-completion bash
Expand All @@ -74,7 +86,7 @@ pmem --install-completion bash

## Memory Commands

All memory commands run under the `memory` group and use the same backend as the Python SDK (same config and storage).
All memory commands run under the `memory` group and use the same backend as the Python SDK (same config and storage). To use a non-default `.env`, pass **global** `--env-file` / `-f` **before** `memory` (see [Global env file placement](#global-env-file-placement)).

### `pmem memory add CONTENT`

Expand Down Expand Up @@ -127,12 +139,15 @@ Search memories by semantic similarity to the given query.
| `--filters JSON` | `-f` | Additional filters as JSON. |
| `--json` | `-j` | Output in JSON. |

**Note:** On this subcommand, **`-f` is `--filters`**, not the global env file. To use a specific `.env`, run `pmem -f /path/.env memory search "…"` (global option before `memory`).

**Examples:**

```bash
pmem memory search "user preferences" --user-id user123
pmem memory search "dark mode" -l 5 -j
pmem memory search "123" -t 0.3
pmem -f .env.production memory search "preferences" --user-id user123
```

---
Expand Down Expand Up @@ -266,7 +281,7 @@ pmem memory delete-all --run-id session1 --confirm

## Configuration Commands

Configuration commands use the same `.env`-based setup as the SDK. Use `--env-file` to point to a specific file.
Configuration commands use the same `.env`-based setup as the SDK. Use **global** `--env-file` / `-f` **before** `config` for any subcommand, or the subcommand’s own `--env-file` / `-f` on **`config validate`** and **`config init`** (see [Global env file placement](#global-env-file-placement)).

### `pmem config show`

Expand All @@ -286,6 +301,7 @@ Display current configuration (from the chosen `.env` file). Sensitive values (e
pmem config show
pmem config show --section llm
pmem config show -j
pmem -f .env.production config show
```

---
Expand Down Expand Up @@ -556,7 +572,7 @@ Bash/Zsh scripts are written under `~/.config/powermem/` and, if you confirm, a

## Summary

- Use **`pmem`** (or **`powermem-cli`**) with **global options** (`-e`, `-j`, `-v`) and **subcommands** for memory, config, stats, manage, and shell.
- Use **`pmem`** (or **`powermem-cli`**) with **global options** placed **before** the subcommand: **`-f` / `--env-file`** (applies to all command groups for that run), **`-j` / `--json`**, **`-v` / `--verbose`**, plus **memory**, **config**, **stats**, **manage**, and **shell** as subcommands.
- **Memory operations**: `memory add/search/get/update/delete/list/delete-all` with filters and JSON output.
- **Configuration**: `config show/validate/test/init` for inspecting, validating, testing, and interactively creating `.env`.
- **Statistics**: `stats` with optional user/agent filters and `--detailed`.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "powermem"
version = "1.0.1"
version = "1.0.2"
description = "Intelligent Memory System - Persistent memory layer for LLM applications"
readme = "README.md"
license = {text = "Apache-2.0"}
Expand Down
15 changes: 13 additions & 2 deletions src/powermem/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,12 @@ def json_option(f):

@click.group(invoke_without_command=True)
@click.option(
"--env-file", "-e",
"--env-file", "-f",
type=click.Path(exists=True),
help="Path to .env configuration file"
help=(
"Load settings from this .env file. Must be placed before the "
"subcommand; applies to memory, config, stats, manage, and shell."
),
)
@click.option(
"--json", "-j", "json_output",
Expand Down Expand Up @@ -121,6 +124,14 @@ def cli(ctx, env_file, json_output, verbose, install_completion):
pmem stats --json
pmem config show

\b
Choosing a .env file (global -f / --env-file):
Put the option before the subcommand so all commands use that file,
e.g. pmem -f .env.production memory list. For "memory search", -f
after the subcommand is --filters (JSON), not the env file—use
pmem -f path/to/.env memory search "query" or the long form
--env-file before "memory".

\b
Shell Completion:
pmem --install-completion bash # Install bash completion
Expand Down
22 changes: 18 additions & 4 deletions src/powermem/config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
or other sources. It simplifies the configuration setup process.
"""

from typing import Any, Dict, Optional
import os
import warnings
from typing import Any, Dict, Optional

from pydantic import AliasChoices, BaseModel, ConfigDict, Field
from pydantic_settings import BaseSettings
Expand All @@ -19,13 +20,26 @@


def _load_dotenv_if_available() -> None:
if not _DEFAULT_ENV_FILE:
return
"""
Load env files into os.environ before BaseSettings / Memory read configuration.

When the CLI passes ``--env-file``, it sets ``POWERMEM_ENV_FILE``; that path
must be loaded here. Otherwise only the auto-detected project ``.env`` is used
and custom paths are silently ignored.
"""
try:
from dotenv import load_dotenv
except Exception:
return
load_dotenv(_DEFAULT_ENV_FILE, override=False)

cli_env = os.environ.get("POWERMEM_ENV_FILE")
if cli_env:
path = os.path.expanduser(os.path.expandvars(cli_env.strip()))
if path and os.path.isfile(path):
load_dotenv(path, override=False)

if _DEFAULT_ENV_FILE:
load_dotenv(_DEFAULT_ENV_FILE, override=False)


class _BasePowermemSettings(BaseSettings):
Expand Down
2 changes: 1 addition & 1 deletion src/powermem/core/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def log_event(
"user_id": user_id,
"agent_id": agent_id,
"details": details,
"version": "1.0.1",
"version": "1.0.2",
}

# Log to file
Expand Down
4 changes: 2 additions & 2 deletions src/powermem/core/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def capture_event(
"user_id": user_id,
"agent_id": agent_id,
"timestamp": get_current_datetime().isoformat(),
"version": "1.0.1",
"version": "1.0.2",
}

self.events.append(event)
Expand Down Expand Up @@ -182,7 +182,7 @@ def set_user_properties(self, user_id: str, properties: Dict[str, Any]) -> None:
"properties": properties,
"user_id": user_id,
"timestamp": get_current_datetime().isoformat(),
"version": "1.0.1",
"version": "1.0.2",
}

self.events.append(event)
Expand Down
2 changes: 1 addition & 1 deletion src/powermem/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Version information management
"""

__version__ = "1.0.1"
__version__ = "1.0.2"
__version_info__ = tuple(map(int, __version__.split(".")))

# Version history
Expand Down
2 changes: 1 addition & 1 deletion tests/regression/test_powermem_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def run_command(self, cmd: str, timeout: int = 60, input_text: str = None) -> Tu

def pmem(self, args: str, timeout: int = 60, input_text: str = None) -> Tuple[int, str, str]:
"""Execute pmem command"""
cmd = f"pmem -e {self.env_file} {args}"
cmd = f"pmem -f {self.env_file} {args}"
return self.run_command(cmd, timeout, input_text)


Expand Down
24 changes: 24 additions & 0 deletions tests/unit/test_config_loader.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os

import powermem.config_loader as config_loader
import powermem.settings as settings

Expand Down Expand Up @@ -186,3 +188,25 @@ def test_load_config_from_env_embedding_common_override(monkeypatch):

assert config["embedder"]["provider"] == "azure_openai"
assert config["embedder"]["config"]["api_key"] == "common-key"


def test_load_dotenv_uses_powermem_env_file(monkeypatch, tmp_path):
monkeypatch.delenv("DASHSCOPE_API_KEY", raising=False)
monkeypatch.setattr(config_loader, "_DEFAULT_ENV_FILE", None, raising=False)
env_path = tmp_path / "powermem.env"
env_path.write_text("DASHSCOPE_API_KEY=key-from-cli-env-file\n", encoding="utf-8")
monkeypatch.setenv("POWERMEM_ENV_FILE", str(env_path))
config_loader._load_dotenv_if_available()
assert os.environ.get("DASHSCOPE_API_KEY") == "key-from-cli-env-file"


def test_load_dotenv_powermem_env_file_wins_over_default(monkeypatch, tmp_path):
monkeypatch.delenv("DASHSCOPE_API_KEY", raising=False)
default = tmp_path / ".env"
default.write_text("DASHSCOPE_API_KEY=from-default\n", encoding="utf-8")
custom = tmp_path / "custom.env"
custom.write_text("DASHSCOPE_API_KEY=from-custom\n", encoding="utf-8")
monkeypatch.setattr(config_loader, "_DEFAULT_ENV_FILE", str(default), raising=False)
monkeypatch.setenv("POWERMEM_ENV_FILE", str(custom))
config_loader._load_dotenv_if_available()
assert os.environ.get("DASHSCOPE_API_KEY") == "from-custom"
16 changes: 8 additions & 8 deletions tests/unit/test_qwen.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,17 +318,17 @@ def test_response_format_parameter(mock_dashscope_generation):
assert call_args["response_format"] == response_format


def test_missing_api_key():
# Test without API key
def test_missing_api_key(monkeypatch):
# QwenConfig reads api_key from LLM_API_KEY / QWEN_API_KEY / DASHSCOPE_API_KEY;
# clear all so CI or local shells do not accidentally satisfy the key.
for key in ("DASHSCOPE_API_KEY", "LLM_API_KEY", "QWEN_API_KEY"):
monkeypatch.delenv(key, raising=False)

config = QwenConfig(model="qwen-turbo")

# Clear environment variable
if "DASHSCOPE_API_KEY" in os.environ:
del os.environ["DASHSCOPE_API_KEY"]


with pytest.raises(ValueError) as exc_info:
QwenLLM(config)

assert "API key is required" in str(exc_info.value)


Expand Down
Loading