# NotiLens

Send notifications from AI agents and any Python project to [NotiLens](https://www.notilens.com).

Two ways to use it — pick one or both:

- **CLI** — for shell scripts, Claude Code hooks, bash pipelines
- **SDK** — for Python projects, with optional AI framework auto-patching

---

## Installation

```bash
pip install notilens
```

With AI framework auto-patching:
```bash
pip install notilens[openai]       # OpenAI
pip install notilens[anthropic]    # Anthropic
pip install notilens[langchain]    # LangChain
pip install notilens[all]          # all frameworks
```

---

---

# CLI

Use the CLI in shell scripts, Claude Code hooks, or any terminal workflow.

## 1. Setup (required, one time)

Get your token and secret from the [NotiLens dashboard](https://www.notilens.com).

```bash
notilens init --agent my-agent --token YOUR_TOKEN --secret YOUR_SECRET
```

This saves credentials to `~/.notilens_config.json`. All future commands for this agent read from there — no need to pass token/secret again.

**Multiple agents** (each agent notifies a different topic):
```bash
notilens init --agent scraper --token TOKEN_A --secret SECRET_A
notilens init --agent mailer  --token TOKEN_B --secret SECRET_B
```

---

## 2. Commands

### Task Lifecycle

```bash
# required: --agent
# optional: --task (auto-generated if omitted)

notilens task.start    --agent my-agent --task job_001
notilens task.progress "Fetching data"  --agent my-agent --task job_001
notilens task.loop     "Step 3 of 10"   --agent my-agent --task job_001
notilens task.retry    --agent my-agent --task job_001
notilens task.stop     --agent my-agent --task job_001
notilens task.complete "All done"       --agent my-agent --task job_001
notilens task.error    "Step 3 failed"  --agent my-agent --task job_001
notilens task.fail     "Unrecoverable"  --agent my-agent --task job_001
notilens task.timeout  "Took too long"  --agent my-agent --task job_001
notilens task.cancel   "User cancelled" --agent my-agent --task job_001
notilens task.terminate "Out of memory" --agent my-agent --task job_001
```

### Input / Human-in-the-loop

```bash
notilens input.required "Please confirm the output" --agent my-agent --task job_001
notilens input.approve  "Confirmed"                 --agent my-agent --task job_001
notilens input.reject   "Rejected by user"          --agent my-agent --task job_001
```

### Output Events

```bash
notilens output.generate "Report ready"     --agent my-agent --task job_001
notilens output.fail     "Model unavailable" --agent my-agent --task job_001
```

### Metrics

Pass any key=value pairs — numeric values accumulate across calls:

```bash
notilens metric tokens=512 cost=0.003 --agent my-agent --task job_001
notilens metric records=1500          --agent my-agent --task job_001

# Reset one metric
notilens metric.reset tokens --agent my-agent --task job_001

# Reset all metrics
notilens metric.reset --agent my-agent --task job_001
```

### Custom Events

Works for any project — AI or not:

```bash
notilens emit user.registered "New signup"      --agent my-agent
notilens emit disk.space.full "Only 2GB left"   --agent my-agent
notilens emit order.placed    "Order #1234"      --agent my-agent
```

---

## 3. Full CLI Example

```bash
# Register once
notilens init --agent summarizer --token my_token --secret my_secret

# Run a job
notilens task.start --agent summarizer --task job_42

notilens metric tokens=1024 --agent summarizer --task job_42
notilens metric cost=0.004  --agent summarizer --task job_42

notilens task.complete "Summary ready" \
  --agent summarizer \
  --task job_42 \
  --open_url https://example.com/summary.pdf \
  --meta pages=12
```

---

## 4. Claude Code Hooks Example

Register the agent once:
```bash
notilens init --agent claude-code --token YOUR_TOKEN --secret YOUR_SECRET
```

Then in `~/.claude/settings.json`:
```json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "notilens task.progress \"Using tool: $CLAUDE_TOOL_NAME\" --agent claude-code --task $CLAUDE_SESSION_ID"
      }]
    }],
    "Stop": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "notilens task.complete \"Session ended\" --agent claude-code --task $CLAUDE_SESSION_ID"
      }]
    }]
  }
}
```

---

## CLI Options

| Flag | Required | Description |
|------|----------|-------------|
| `--agent NAME` | Yes | Agent name |
| `--task ID` | No | Task ID — auto-generated if omitted |
| `--level` | No | Override level: `debug` `info` `warning` `error` |
| `--meta key=value` | No | Custom metadata (repeatable) |
| `--image_url URL` | No | Attach an image |
| `--open_url URL` | No | Link to open |
| `--download_url URL` | No | Link to download |
| `--tags "tag1,tag2"` | No | Comma-separated tags |
| `--is_actionable true\|false` | No | Override actionable flag |

---

---

# SDK

Use the SDK in Python projects. Supports manual task lifecycle calls and optional auto-patching of AI frameworks.

## 1. Setup (required)

```python
import notilens

# required: agent, token, secret
# token/secret can also come from NOTILENS_TOKEN / NOTILENS_SECRET env vars
agent = notilens.init(
    agent="my-agent",    # required — agent name
    token="YOUR_TOKEN",  # required — or set NOTILENS_TOKEN env var
    secret="YOUR_SECRET" # required — or set NOTILENS_SECRET env var
)
```

**Via environment variables:**
```bash
export NOTILENS_TOKEN=your_token
export NOTILENS_SECRET=your_secret
```
```python
agent = notilens.init(agent="my-agent")  # reads token+secret from env
```

**All init options:**
```python
agent = notilens.init(
    agent="my-agent",      # required
    token="...",           # required (or env var)
    secret="...",          # required (or env var)
    patch=False,           # optional — auto-patch AI frameworks (default: False)
    min_level="info",      # optional — minimum event level to send (default: "info")
    loop_threshold=10,     # optional — AI calls before loop alert (default: 10)
    loop_window=60.0,      # optional — loop detection window in seconds (default: 60)
    call_timeout=30.0,     # optional — alert if AI call exceeds N seconds (default: 30)
    silent=False,          # optional — suppress SDK log output (default: False)
    debug=False,           # optional — verbose logging (default: False)
)
```

---

## 2. Task Lifecycle

```python
# task_id is optional — auto-generated if omitted
# task_start returns the task_id so you can reference it later
task_id = agent.task_start(task_id="job_001")  # task_id optional

agent.task_progress("Step 2 of 5", task_id=task_id)   # optional mid-run update
agent.task_loop("Processing item 42", task_id=task_id) # optional loop marker
agent.task_retry(task_id=task_id)                      # optional retry signal

# Terminal events — pick one to end the task
agent.task_complete("All done", task_id=task_id)
agent.task_fail("Unrecoverable error", task_id=task_id)
agent.task_timeout("Exceeded time limit", task_id=task_id)
agent.task_cancel("User cancelled", task_id=task_id)
agent.task_terminate("OOM", task_id=task_id)
agent.task_stop(task_id=task_id)

# Non-terminal error (task continues)
agent.task_error("Step 3 failed, retrying", task_id=task_id)
```

---

## 3. Input / Human-in-the-loop

```python
agent.input_required("Confirm before proceeding", task_id=task_id)
agent.input_approved("User confirmed", task_id=task_id)
agent.input_rejected("User rejected", task_id=task_id)
```

---

## 4. Output Events

```python
# Use for any kind of generated output — AI response, report, file, API result
agent.output_generated("Summary ready", task_id=task_id)
agent.output_failed("Model unavailable", task_id=task_id)
```

---

## 5. Metrics

Track any numeric or string values — accumulated automatically and included in every notification.

```python
agent.metric("tokens", 350)    # set
agent.metric("tokens", 210)    # now 560 (numeric values accumulate)
agent.metric("cost", 0.0012)
agent.metric("records", 1500)
agent.metric("model", "gpt-4") # strings are replaced, not accumulated

agent.reset_metrics("tokens")  # reset one metric
agent.reset_metrics()           # reset all metrics
```

---

## 6. Custom Events

Works for any project — AI or not:

```python
agent.emit("user.registered", "New signup", meta={"plan": "pro"})  # meta optional
agent.emit("disk.space.full", "Only 2GB left", level="warning")    # level optional
agent.emit("order.placed", "Order #1234", meta={"amount": 99.99})
```

---

## 7. Auto-patching AI Frameworks

Add `patch=True` to `init()` — no other changes needed. NotiLens will automatically track every AI call.

```python
import notilens
import openai  # or anthropic, langchain, crewai, pydantic-ai

agent = notilens.init(
    agent="my-agent",
    token="YOUR_TOKEN",
    secret="YOUR_SECRET",
    patch=True,           # required to enable auto-patching
    call_timeout=30.0,    # optional — alert if any AI call takes longer than 30s
    loop_threshold=10,    # optional — alert if 10+ AI calls happen within loop_window
)

# From here, use OpenAI / Anthropic etc. normally.
# NotiLens fires ai.call.start, ai.call.complete, task.error, task.timeout, task.loop automatically.
response = openai.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Summarise this..."}],
)
```

**Multiple agents — only one can own patching:**
```python
scraper = notilens.init(agent="scraper", token="TOKEN_A", secret="SECRET_A", patch=True)
mailer  = notilens.init(agent="mailer",  token="TOKEN_B", secret="SECRET_B")
# patch=True on a second agent raises RuntimeError
```

---

## 8. Full SDK Example

```python
import notilens

agent   = notilens.init("summarizer", token="my_token", secret="my_secret")
task_id = agent.task_start()

try:
    agent.task_progress("Fetching PDF", task_id=task_id)

    result = llm.complete(prompt)
    agent.metric("tokens", result.usage.total_tokens)
    agent.metric("cost", result.usage.cost)

    agent.output_generated("Summary ready", task_id=task_id)
    agent.task_complete("All done", task_id=task_id)

except Exception as e:
    agent.task_fail(str(e), task_id=task_id)
```

---

---

# Events Reference

| Event | Default Type | Description |
|-------|-------------|-------------|
| `task.started` | info | Task began |
| `task.progress` | info | Mid-run update |
| `task.loop` | warning | Loop iteration |
| `task.retry` | warning | Retry attempt |
| `task.completed` | success | Task finished successfully |
| `task.stopped` | info | Manually stopped |
| `task.failed` | urgent | Task failed |
| `task.error` | urgent | Non-fatal error |
| `task.timeout` | urgent | Exceeded time limit |
| `task.cancelled` | warning | Task cancelled |
| `task.terminated` | urgent | Force-terminated |
| `output.generated` | success | Output produced (AI response, report, file, etc.) |
| `output.failed` | urgent | Output generation failed |
| `input.required` | warning | Waiting for human input |
| `input.approved` | success | Input approved |
| `input.rejected` | warning | Input rejected |

---

## Requirements

- Python >= 3.9

## License

MIT — [notilens.com](https://www.notilens.com)
