# NotiLens

Unified SDK + CLI for sending notifications from AI agents and any Python project to [NotiLens](https://www.notilens.com).

## Installation

```bash
pip install notilens
```

With framework auto-patching:
```bash
pip install notilens[openai]
pip install notilens[anthropic]
pip install notilens[all]        # all frameworks
```

---

## Quick Start

### CLI (hooks, shell scripts)

```bash
# Register your agent
notilens init --agent my-agent --token YOUR_TOKEN --secret YOUR_SECRET

# Use in scripts / Claude Code hooks
notilens task.start --agent my-agent --task job_001
notilens task.complete "All done" --agent my-agent --task job_001
```

### Python SDK

```python
import notilens

agent = notilens.init(agent="my-agent", token="YOUR_TOKEN", secret="YOUR_SECRET")

agent.task_start(task_id="job_001")
agent.task_complete("All done", task_id="job_001")
```

### Python SDK with auto-patching

```python
import notilens

agent = notilens.init(
    agent="my-agent",
    token="YOUR_TOKEN",
    secret="YOUR_SECRET",
    patch=True,   # auto-instruments OpenAI, Anthropic, LangChain, CrewAI, etc.
)

# Use OpenAI, Anthropic etc. normally — events fire automatically
```

---

## Setup

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

### CLI
```bash
notilens init --agent AGENT --token TOKEN --secret SECRET
```

### SDK (env vars)
```bash
export NOTILENS_TOKEN=your_token
export NOTILENS_SECRET=your_secret
```
```python
agent = notilens.init(agent="my-agent")  # reads from env
```

### Multiple agents
```bash
notilens init --agent scraper --token TOKEN_A --secret SECRET_A
notilens init --agent mailer  --token TOKEN_B --secret SECRET_B
```
```python
scraper = notilens.init(agent="scraper", token="TOKEN_A", secret="SECRET_A")
mailer  = notilens.init(agent="mailer",  token="TOKEN_B", secret="SECRET_B", patch=True)
```

Only one agent can own `patch=True` — raises if more than one tries.

---

## CLI Reference

### Task Lifecycle

```bash
notilens task.start    --agent AGENT [--task ID]
notilens task.progress "msg"  --agent AGENT --task ID
notilens task.loop     "msg"  --agent AGENT --task ID
notilens task.retry    --agent AGENT --task ID
notilens task.stop     --agent AGENT --task ID
notilens task.complete "msg"  --agent AGENT --task ID
notilens task.error    "msg"  --agent AGENT --task ID
notilens task.fail     "msg"  --agent AGENT --task ID
notilens task.timeout  "msg"  --agent AGENT --task ID
notilens task.cancel   "msg"  --agent AGENT --task ID
notilens task.terminate "msg" --agent AGENT --task ID
```

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

```bash
notilens input.required "Please confirm" --agent AGENT --task ID
notilens input.approve  "Approved"       --agent AGENT --task ID
notilens input.reject   "Rejected"       --agent AGENT --task ID
```

### AI Response

```bash
notilens ai.response.generate "Summary ready" --agent AGENT --task ID
notilens ai.response.fail     "Model error"   --agent AGENT --task ID
```

### Generic Event

Emit any custom event — works for non-AI projects too:

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

### Agent Management

```bash
notilens agents                           # list configured agents
notilens remove-agent AGENT              # remove an agent
notilens version
```

---

## SDK Reference

### `notilens.init()`

```python
agent = notilens.init(
    agent="my-agent",         # agent name (required)
    token="...",              # NotiLens token (or NOTILENS_TOKEN env var)
    secret="...",             # NotiLens secret (or NOTILENS_SECRET env var)
    patch=False,              # auto-patch AI frameworks
    min_level="info",         # minimum event level to send
    loop_threshold=10,        # AI calls within loop_window before loop alert
    loop_window=60.0,         # time window in seconds for loop detection
    call_timeout=30.0,        # alert if AI call exceeds this many seconds
    silent=False,             # suppress SDK log output
    debug=False,              # enable verbose logging
)
```

### Task Lifecycle

```python
task_id = agent.task_start(task_id="job_001")   # task_id auto-generated if omitted
agent.task_progress("Step 2 of 5", task_id=task_id)
agent.task_loop("Processing item 42", task_id=task_id)
agent.task_retry(task_id=task_id)
agent.task_stop(task_id=task_id)
agent.task_complete("Done", task_id=task_id)
agent.task_error("Step failed", task_id=task_id)
agent.task_fail("Unrecoverable", task_id=task_id)
agent.task_timeout("Exceeded limit", task_id=task_id)
agent.task_cancel("User cancelled", task_id=task_id)
agent.task_terminate("OOM", task_id=task_id)
```

### 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)
```

### AI Response

```python
agent.ai_response_generated("Summary ready", task_id=task_id)
agent.ai_response_failed("Model unavailable", task_id=task_id)
```

### Generic Emit

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

---

## CLI Options

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

---

## Events

| Event | Level | Description |
|-------|-------|-------------|
| `task.started` | info | Task began |
| `task.progress` | info | Mid-run update |
| `task.loop` | warning | Loop iteration |
| `task.retrying` | warning | Retry attempt |
| `task.completed` | info | Task finished successfully |
| `task.stopped` | info | Manually stopped |
| `task.failed` | error | Task failed |
| `task.error` | error | Non-fatal error |
| `task.timeout` | error | Exceeded time limit |
| `task.cancelled` | warning | Task cancelled |
| `task.terminated` | error | Force-terminated |
| `ai.call.start` | info | AI framework call began |
| `ai.call.complete` | info | AI framework call finished |
| `ai.response.generated` | info | AI produced a response |
| `ai.response.failed` | error | AI failed to respond |
| `input.required` | warning | Waiting for human input |
| `input.approved` | info | Input approved |
| `input.rejected` | warning | Input rejected |

---

## Full Example

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

# Run a job
notilens task.start --agent summarizer --task job_42
notilens task.progress "Fetching document" --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
```

```python
import notilens
import openai

agent = notilens.init(
    agent="summarizer",
    token="my_token",
    secret="my_secret",
    patch=True,
)

task_id = agent.task_start()
try:
    # OpenAI call tracked automatically
    response = openai.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": "Summarise this document..."}],
    )
    agent.ai_response_generated(response.choices[0].message.content[:80], task_id=task_id)
    agent.task_complete("Summary done", task_id=task_id)
except Exception as e:
    agent.task_fail(str(e), task_id=task_id)
```

---

## License

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